Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4acaa2086 | ||
|
|
00e38a7149 | ||
|
|
04e14f32bf | ||
|
|
56795bbcba | ||
|
|
03bbe79320 | ||
|
|
9ef0de4848 | ||
|
|
4e4e3ef659 | ||
|
|
43cdaec895 | ||
|
|
fde6de3fcf | ||
|
|
33d3e032b3 | ||
|
|
03479cf6b3 | ||
|
|
b8a88fe89c | ||
|
|
409ac30c91 | ||
|
|
a59ebadd34 | ||
|
|
174ebfad56 | ||
|
|
008e048968 | ||
|
|
7e15fcc080 | ||
|
|
6636b8882f | ||
|
|
6398fc3cba | ||
|
|
fc698bd9b4 | ||
|
|
cd61e8dd44 | ||
|
|
cbcdd86569 | ||
|
|
553f01fde1 | ||
|
|
d8f58d28ed | ||
|
|
b194089fec | ||
|
|
927da438c7 | ||
|
|
9c21fd320c | ||
|
|
f142266bed | ||
|
|
4b06bc4d2d | ||
|
|
d692db47b2 | ||
|
|
3391e8580c |
@@ -16,3 +16,48 @@ 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,7 +4,14 @@ 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?"
|
||||
@@ -31,13 +38,17 @@ jobs:
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Enable Corepack and pnpm
|
||||
run: corepack enable pnpm
|
||||
|
||||
- name: Build web apps
|
||||
run: yarn run build
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint web apps
|
||||
run: yarn run ci:lint
|
||||
run: pnpm run check:lint
|
||||
|
||||
- name: Check format
|
||||
run: pnpm run check:format
|
||||
|
||||
- name: Build apps
|
||||
run: pnpm run build
|
||||
|
||||
+5
-3
@@ -24,11 +24,13 @@ 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
|
||||
@@ -60,6 +62,7 @@ node_modules/
|
||||
assets/dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
pnpm-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
@@ -75,10 +78,9 @@ package-lock.json
|
||||
|
||||
# lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
|
||||
.npmrc
|
||||
|
||||
|
||||
.secrets
|
||||
tmp/
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
|
||||
# Enforce Node version for consistent installs
|
||||
use-node-version=22.18.0
|
||||
|
||||
# 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 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
+1
-1
@@ -73,7 +73,7 @@ docker compose -f docker-compose-local.yml up
|
||||
4. Start web apps:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
5. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
dist/
|
||||
build/
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
# *****************************************************************************
|
||||
@@ -7,7 +13,8 @@ FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=admin --docker
|
||||
@@ -22,11 +29,13 @@ WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install --network-timeout 500000
|
||||
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/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
|
||||
@@ -49,7 +58,7 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN yarn turbo run build --filter=admin
|
||||
RUN pnpm turbo run build --filter=admin
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
@@ -91,4 +100,4 @@ ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/admin/server.js"]
|
||||
CMD ["node", "apps/admin/server.js"]
|
||||
|
||||
@@ -5,8 +5,8 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
|
||||
@@ -14,4 +14,4 @@ EXPOSE 3000
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
|
||||
|
||||
CMD ["yarn", "dev", "--filter=admin"]
|
||||
CMD ["pnpm", "dev", "--filter=admin"]
|
||||
|
||||
+12
-12
@@ -18,15 +18,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/constants": "*",
|
||||
"@plane/hooks": "*",
|
||||
"@plane/propel": "*",
|
||||
"@plane/services": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/utils": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@plane/constants": "workspace:*",
|
||||
"@plane/hooks": "workspace:*",
|
||||
"@plane/propel": "workspace:*",
|
||||
"@plane/services": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -39,13 +37,15 @@
|
||||
"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": "*",
|
||||
"@plane/tailwind-config": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
|
||||
@@ -89,7 +89,9 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]),
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(
|
||||
http_method_names=["get", "patch", "delete"]
|
||||
),
|
||||
name="issue-attachment",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,6 +145,22 @@ 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):
|
||||
"""
|
||||
@@ -331,6 +347,10 @@ 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 = (
|
||||
@@ -390,6 +410,7 @@ 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,
|
||||
@@ -1782,7 +1803,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
model = FileAsset
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
use_read_replica = True
|
||||
|
||||
@issue_attachment_docs(
|
||||
@@ -1865,6 +1885,22 @@ 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")
|
||||
@@ -1989,7 +2025,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
"""Issue Attachment Detail Endpoint"""
|
||||
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
model = FileAsset
|
||||
use_read_replica = True
|
||||
|
||||
@@ -2012,6 +2047,22 @@ 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
|
||||
)
|
||||
@@ -2074,6 +2125,19 @@ 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
|
||||
@@ -2128,6 +2192,23 @@ 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
|
||||
)
|
||||
|
||||
@@ -7,16 +7,31 @@ from plane.utils.url import contains_url
|
||||
|
||||
from .base import BaseSerializer
|
||||
|
||||
# Django import
|
||||
import re
|
||||
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
def validate_first_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("First name cannot contain a URL.")
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9_\- ]+$", value):
|
||||
raise serializers.ValidationError(
|
||||
"first name can only contain letters, numbers, hyphens (-), and underscores (_)"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def validate_last_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Last name cannot contain a URL.")
|
||||
|
||||
if not re.match(r"^[a-zA-Z0-9_\- ]+$", value) and not value == "":
|
||||
raise serializers.ValidationError(
|
||||
"last name can only contain letters, numbers, hyphens (-), and underscores (_)"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -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
|
||||
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery, Count
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@@ -69,25 +69,31 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
link_count=Subquery(
|
||||
IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.values("issue")
|
||||
.annotate(count=Count("id"))
|
||||
.values("count")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
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")
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Subquery(
|
||||
Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.values("parent")
|
||||
.annotate(count=Count("id"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -101,6 +107,19 @@ 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"
|
||||
@@ -136,6 +155,7 @@ 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
|
||||
),
|
||||
@@ -170,6 +190,7 @@ 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
|
||||
),
|
||||
@@ -196,6 +217,7 @@ 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
|
||||
),
|
||||
|
||||
@@ -213,27 +213,33 @@ class IssueViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=FileAsset.objects.filter(
|
||||
issue_id=OuterRef("id"),
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
link_count=Subquery(
|
||||
IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.values("issue")
|
||||
.annotate(count=Count("id"))
|
||||
.values("count")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
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")
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
.annotate(
|
||||
sub_issues_count=Subquery(
|
||||
Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.values("parent")
|
||||
.annotate(count=Count("id"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@@ -249,6 +255,10 @@ 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
|
||||
@@ -281,6 +291,7 @@ 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:
|
||||
@@ -296,6 +307,7 @@ 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
|
||||
),
|
||||
@@ -329,6 +341,7 @@ 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
|
||||
),
|
||||
@@ -354,6 +367,7 @@ 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
|
||||
),
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# 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",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -83,3 +83,5 @@ from .label import Label
|
||||
from .device import Device, DeviceSession
|
||||
|
||||
from .sticky import Sticky
|
||||
|
||||
from .description import Description, DescriptionVersion
|
||||
@@ -0,0 +1,56 @@
|
||||
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,11 +2,12 @@
|
||||
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
|
||||
@@ -20,21 +21,38 @@ class Command(BaseCommand):
|
||||
# Positional argument
|
||||
parser.add_argument("machine_signature", type=str, help="Machine signature")
|
||||
|
||||
def read_package_json(self):
|
||||
with open("package.json", "r") as file:
|
||||
# Load JSON content from the file
|
||||
data = json.load(file)
|
||||
def check_for_current_version(self):
|
||||
if os.environ.get("APP_VERSION", False):
|
||||
return os.environ.get("APP_VERSION")
|
||||
|
||||
payload = {
|
||||
"instance_key": settings.INSTANCE_KEY,
|
||||
"version": data.get("version", 0.1),
|
||||
}
|
||||
return payload
|
||||
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
|
||||
|
||||
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")
|
||||
@@ -42,13 +60,11 @@ 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=payload.get("version"),
|
||||
latest_version=payload.get("version"),
|
||||
current_version=current_version,
|
||||
latest_version=latest_version,
|
||||
last_checked_at=timezone.now(),
|
||||
is_test=os.environ.get("IS_TEST", "0") == "1",
|
||||
edition=InstanceEdition.PLANE_COMMUNITY.value,
|
||||
@@ -57,11 +73,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 = payload.get("version")
|
||||
instance.latest_version = payload.get("version")
|
||||
instance.current_version = current_version
|
||||
instance.latest_version = latest_version
|
||||
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
|
||||
instance.edition = InstanceEdition.PLANE_COMMUNITY.value
|
||||
instance.save()
|
||||
|
||||
@@ -304,16 +304,10 @@ 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"
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import pytest
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.app.serializers.user import UserSerializer
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUserSerializer:
|
||||
"""Test the UserSerializer"""
|
||||
|
||||
def test_validate_first_name_valid(self):
|
||||
"""Test that valid first names are accepted"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
valid_names = [
|
||||
"John",
|
||||
"John Doe",
|
||||
"John-Doe",
|
||||
"John_Doe",
|
||||
"John123",
|
||||
]
|
||||
|
||||
for name in valid_names:
|
||||
result = serializer.validate_first_name(name)
|
||||
|
||||
assert result == name
|
||||
|
||||
def test_validate_first_name_with_url(self):
|
||||
"""Test that first names containing URLs are rejected"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
invalid_names = [
|
||||
"http://example.com",
|
||||
"John https://test.com",
|
||||
"www.test.com",
|
||||
]
|
||||
|
||||
for name in invalid_names:
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
serializer.validate_first_name(name)
|
||||
|
||||
assert str(exc_info.value.detail[0]) == "First name cannot contain a URL."
|
||||
|
||||
def test_validate_first_name_with_special_chars(self):
|
||||
"""Test that first names with special characters are rejected"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
invalid_names = [
|
||||
"John@Doe",
|
||||
"John#Doe",
|
||||
"John$Doe",
|
||||
"John!Doe",
|
||||
"John&Doe",
|
||||
]
|
||||
|
||||
for name in invalid_names:
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
serializer.validate_first_name(name)
|
||||
|
||||
assert str(exc_info.value.detail[0]) == (
|
||||
"first name can only contain letters, numbers, "
|
||||
"hyphens (-), and underscores (_)"
|
||||
)
|
||||
|
||||
def test_validate_last_name_valid(self):
|
||||
"""Test that valid last names are accepted"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
valid_names = [
|
||||
"Smith",
|
||||
"Smith Jr",
|
||||
"Smith-Jr",
|
||||
"Smith_Jr",
|
||||
"Smith123",
|
||||
"",
|
||||
]
|
||||
|
||||
for name in valid_names:
|
||||
result = serializer.validate_last_name(name)
|
||||
|
||||
assert result == name
|
||||
|
||||
def test_validate_last_name_with_url(self):
|
||||
"""Test that last names containing URLs are rejected"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
invalid_names = [
|
||||
"http://example.com",
|
||||
"Smith https://test.com",
|
||||
"www.test.com",
|
||||
]
|
||||
|
||||
for name in invalid_names:
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
serializer.validate_last_name(name)
|
||||
|
||||
assert str(exc_info.value.detail[0]) == "Last name cannot contain a URL."
|
||||
|
||||
def test_validate_last_name_with_special_chars(self):
|
||||
"""Test that last names with special characters are rejected"""
|
||||
|
||||
serializer = UserSerializer()
|
||||
invalid_names = [
|
||||
"Smith@Jr",
|
||||
"Smith#Jr",
|
||||
"Smith$Jr",
|
||||
"Smith!Jr",
|
||||
"Smith&Jr",
|
||||
]
|
||||
|
||||
for name in invalid_names:
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
serializer.validate_last_name(name)
|
||||
|
||||
assert str(exc_info.value.detail[0]) == (
|
||||
"last name can only contain letters, numbers, "
|
||||
"hyphens (-), and underscores (_)"
|
||||
)
|
||||
@@ -187,24 +187,6 @@ 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
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class OffsetPaginator:
|
||||
total_count = (
|
||||
self.total_count_queryset.count()
|
||||
if self.total_count_queryset
|
||||
else results.count()
|
||||
else queryset.count()
|
||||
)
|
||||
|
||||
# Check if there are more results available after the current page
|
||||
|
||||
@@ -4,12 +4,12 @@ RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
EXPOSE 3003
|
||||
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/live/node_modules"]
|
||||
|
||||
CMD ["yarn","dev", "--filter=live"]
|
||||
CMD ["pnpm","dev", "--filter=live"]
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
# *****************************************************************************
|
||||
@@ -9,9 +15,10 @@ RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
RUN yarn global add turbo
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
RUN turbo prune live --docker
|
||||
RUN turbo prune --scope=live --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
@@ -25,16 +32,18 @@ WORKDIR /app
|
||||
# First install dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
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
|
||||
|
||||
# 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 yarn turbo build --filter=live
|
||||
RUN pnpm turbo run build --filter=live
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Run the project
|
||||
@@ -44,11 +53,12 @@ FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=installer /app/packages ./packages
|
||||
COPY --from=installer /app/apps/live/dist ./live
|
||||
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/node_modules ./node_modules
|
||||
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "live/server.js"]
|
||||
CMD ["node", "apps/live/dist/server.js"]
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"@hocuspocus/extension-logger": "^2.15.0",
|
||||
"@hocuspocus/extension-redis": "^2.15.0",
|
||||
"@hocuspocus/server": "^2.15.0",
|
||||
"@plane/editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/editor": "workspace:*",
|
||||
"@plane/types": "workspace:*",
|
||||
"@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": "*",
|
||||
"@plane/typescript-config": "*",
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@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",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { pinoHttp } from "pino-http";
|
||||
import { Logger } from "pino";
|
||||
|
||||
const transport = {
|
||||
target: "pino-pretty",
|
||||
@@ -37,4 +36,4 @@ export const logger = pinoHttp({
|
||||
},
|
||||
});
|
||||
|
||||
export const manualLogger: Logger = logger.logger;
|
||||
export const manualLogger: typeof logger.logger = logger.logger;
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
.vercel
|
||||
.tubro
|
||||
out/
|
||||
dis/
|
||||
build/
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
EXPOSE 4000
|
||||
EXPOSE 3002
|
||||
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
|
||||
CMD ["yarn","dev", "--filter=space"]
|
||||
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]
|
||||
|
||||
CMD ["pnpm", "dev", "--filter=space"]
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
# *****************************************************************************
|
||||
@@ -7,7 +13,8 @@ FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
@@ -22,11 +29,13 @@ WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install --network-timeout 500000
|
||||
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/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
|
||||
@@ -49,7 +58,7 @@ ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN yarn turbo run build --filter=space
|
||||
RUN pnpm turbo run build --filter=space
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
@@ -91,4 +100,4 @@ ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/space/server.js"]
|
||||
CMD ["node", "apps/space/server.js"]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner, PoweredBy } from "@/components/common";
|
||||
import { IssuesNavbarRoot } from "@/components/issues";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
import { IssuesNavbarRoot } from "@/components/issues/navbar";
|
||||
// hooks
|
||||
import { useIssueFilter, usePublish, usePublishList } from "@/hooks/store";
|
||||
import { usePublish, usePublishList } from "@/hooks/store/publish";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -4,9 +4,11 @@ import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { IssuesLayoutsRoot } from "@/components/issues";
|
||||
import { IssuesLayoutsRoot } from "@/components/issues/issue-layouts";
|
||||
// hooks
|
||||
import { usePublish, useLabel, useStates } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
type Props = {
|
||||
params: {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { UserLoggedIn } from "@/components/account";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { UserLoggedIn } from "@/components/account/user-logged-in";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { AuthView } from "@/components/views";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
const HomePage = observer(() => {
|
||||
const { data: currentUser, isAuthenticated, isInitializing } = useUser();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
// components
|
||||
import { TranslationProvider } from "@plane/i18n";
|
||||
import { InstanceProvider } from "@/lib/instance-provider";
|
||||
@@ -15,12 +16,14 @@ export const AppProvider: FC<IAppProvider> = (props) => {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<StoreProvider>
|
||||
<TranslationProvider>
|
||||
<ToastProvider>
|
||||
<InstanceProvider>{children}</InstanceProvider>
|
||||
</ToastProvider>
|
||||
</TranslationProvider>
|
||||
</StoreProvider>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<StoreProvider>
|
||||
<TranslationProvider>
|
||||
<ToastProvider>
|
||||
<InstanceProvider>{children}</InstanceProvider>
|
||||
</ToastProvider>
|
||||
</TranslationProvider>
|
||||
</StoreProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner, PoweredBy } from "@/components/common";
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
||||
// hooks
|
||||
import { usePublish, usePublishList } from "@/hooks/store";
|
||||
import { usePublish, usePublishList } from "@/hooks/store/publish";
|
||||
// Plane web
|
||||
import { ViewNavbarRoot } from "@/plane-web/components/navbar";
|
||||
import { useView } from "@/plane-web/hooks/store";
|
||||
@@ -18,7 +19,7 @@ type Props = {
|
||||
};
|
||||
};
|
||||
|
||||
const IssuesLayout = observer((props: Props) => {
|
||||
const ViewsLayout = observer((props: Props) => {
|
||||
const { children, params } = props;
|
||||
// params
|
||||
const { anchor } = params;
|
||||
@@ -61,4 +62,4 @@ const IssuesLayout = observer((props: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default IssuesLayout;
|
||||
export default ViewsLayout;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
// components
|
||||
import { PoweredBy } from "@/components/common";
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
// plane-web
|
||||
import { ViewLayoutsRoot } from "@/plane-web/components/issue-layouts/root";
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
};
|
||||
};
|
||||
|
||||
const IssuesPage = observer((props: Props) => {
|
||||
const ViewsPage = observer((props: Props) => {
|
||||
const { params } = props;
|
||||
const { anchor } = params;
|
||||
// params
|
||||
@@ -34,4 +34,4 @@ const IssuesPage = observer((props: Props) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default IssuesPage;
|
||||
export default ViewsPage;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { PageNotFound } from "@/components/ui/not-found";
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
|
||||
type Props = {
|
||||
peekId: string | undefined;
|
||||
publishSettings: PublishStore;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const ViewLayoutsRoot = (props: Props) => <PageNotFound />;
|
||||
export const ViewLayoutsRoot = (_props: Props) => <PageNotFound />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
|
||||
type Props = {
|
||||
publishSettings: PublishStore;
|
||||
|
||||
@@ -11,14 +11,6 @@ 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,
|
||||
@@ -27,7 +19,7 @@ import {
|
||||
authErrorHandler,
|
||||
} from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
// types
|
||||
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
||||
// assets
|
||||
@@ -35,6 +27,13 @@ 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,8 +1 @@
|
||||
export * from "./auth-root";
|
||||
|
||||
export * from "./auth-header";
|
||||
export * from "./auth-banner";
|
||||
|
||||
export * from "./email";
|
||||
export * from "./password";
|
||||
export * from "./unique-code";
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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";
|
||||
import { UserAvatar } from "@/components/issues";
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
// assets
|
||||
import UserLoggedInImage from "@/public/user-logged-in.svg";
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./project-logo";
|
||||
export * from "./logo-spinner";
|
||||
export * from "./powered-by";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./mentions";
|
||||
@@ -2,7 +2,8 @@ import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./embeds";
|
||||
export * from "./lite-text-editor";
|
||||
export * from "./rich-text-editor";
|
||||
export * from "./toolbar";
|
||||
@@ -3,12 +3,13 @@ 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,14 +1,15 @@
|
||||
import React, { forwardRef } from "react";
|
||||
// plane imports
|
||||
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";
|
||||
import { type EditorRefApi, type IRichTextEditorProps, RichTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||
import type { MakeOptional } from "@plane/types";
|
||||
// helpers
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// store hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
// 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";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler">,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
// plane imports
|
||||
import { TOOLBAR_ITEMS, ToolbarMenuItem, EditorRefApi } from "@plane/editor";
|
||||
import { TOOLBAR_ITEMS, type ToolbarMenuItem, type EditorRefApi } from "@plane/editor";
|
||||
import { Button, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
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";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
// 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";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./dropdown";
|
||||
export * from "./filter-header";
|
||||
export * from "./filter-option";
|
||||
@@ -1,11 +1 @@
|
||||
// filters
|
||||
export * from "./root";
|
||||
export * from "./selection";
|
||||
|
||||
// properties
|
||||
export * from "./state";
|
||||
export * from "./priority";
|
||||
export * from "./labels";
|
||||
|
||||
// helpers
|
||||
export * from "./helpers";
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"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 { IIssueLabel } from "@/types/issue";
|
||||
import type { IIssueLabel } from "@/types/issue";
|
||||
// local imports
|
||||
import { FilterHeader } from "./helpers/filter-header";
|
||||
import { FilterOption } from "./helpers/filter-option";
|
||||
|
||||
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";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "./helpers";
|
||||
// constants
|
||||
// local imports
|
||||
import { FilterHeader } from "./helpers/filter-header";
|
||||
import { FilterOption } from "./helpers/filter-option";
|
||||
|
||||
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";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
// types
|
||||
import { TIssueQueryFilters } from "@/types/issue";
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Search, X } from "lucide-react";
|
||||
// types
|
||||
import { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
// components
|
||||
import { FilterPriority, FilterState } from ".";
|
||||
import type { IIssueFilterOptions, TIssueFilterKeys } from "@/types/issue";
|
||||
// local imports
|
||||
import { FilterPriority } from "./priority";
|
||||
import { FilterState } from "./state";
|
||||
|
||||
type Props = {
|
||||
filters: IIssueFilterOptions;
|
||||
|
||||
@@ -5,10 +5,11 @@ 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";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
// local imports
|
||||
import { FilterHeader } from "./helpers/filter-header";
|
||||
import { FilterOption } from "./helpers/filter-option";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./issue-layouts";
|
||||
export * from "./navbar";
|
||||
@@ -1,4 +1 @@
|
||||
export * from "./kanban/base-kanban-root";
|
||||
export * from "./list/base-list-root";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { TLoader } from "@plane/types";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
// plane imports
|
||||
import type { TLoader } from "@plane/types";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
|
||||
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";
|
||||
import { useIssue } from "@/hooks/store/use-issue";
|
||||
|
||||
import { KanBan } from "./default";
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ import { useParams } from "next/navigation";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
import { IssueEmojiReactions } from "@/components/issues/reactions/issue-emoji-reactions";
|
||||
import { IssueVotes } from "@/components/issues/reactions/issue-vote-reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
|
||||
type Props = {
|
||||
issueId: string;
|
||||
|
||||
@@ -15,7 +15,8 @@ import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
//
|
||||
import { IIssue } from "@/types/issue";
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
TLoader,
|
||||
} from "@plane/types";
|
||||
// hooks
|
||||
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
|
||||
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 { getGroupByColumns } from "../utils";
|
||||
// components
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
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 {
|
||||
import type {
|
||||
TGroupedIssues,
|
||||
IIssueDisplayProperties,
|
||||
TSubGroupedIssues,
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIntersectionObserver } from "@/hooks/use-intersection-observer";
|
||||
//
|
||||
import { KanbanIssueBlocksList } from ".";
|
||||
// local imports
|
||||
import { KanbanIssueBlocksList } from "./blocks-list";
|
||||
|
||||
interface IKanbanGroup {
|
||||
groupId: string;
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
TLoader,
|
||||
} from "@plane/types";
|
||||
// hooks
|
||||
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
|
||||
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 { 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";
|
||||
import { useIssue } from "@/hooks/store/use-issue";
|
||||
import { List } from "./default";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -13,7 +13,8 @@ import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
//
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
TLoader,
|
||||
} from "@plane/types";
|
||||
// hooks
|
||||
import { useMember, useModule, useStates, useLabel, useCycle } from "@/hooks/store";
|
||||
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 { getGroupByColumns } from "../utils";
|
||||
import { ListGroup } from "./list-group";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./block";
|
||||
export * from "./blocks-list";
|
||||
@@ -2,27 +2,23 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Layers, Link, Paperclip } from "lucide-react";
|
||||
// plane types
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// plane ui
|
||||
// plane imports
|
||||
import type { IIssueDisplayProperties } from "@plane/types";
|
||||
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";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
type Props = {
|
||||
due_date: string | undefined;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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,8 +2,10 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { Tags } from "lucide-react";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { useLabel } from "@/hooks/store";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
|
||||
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";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
type Props = {
|
||||
stateId: string | undefined;
|
||||
|
||||
@@ -4,15 +4,18 @@ 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, useIssueDetails, useIssueFilter } from "@/hooks/store";
|
||||
import { useIssue } from "@/hooks/store/use-issue";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
// assets
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
// local imports
|
||||
import { SomethingWentWrongError } from "./error";
|
||||
import { IssueKanbanLayoutRoot } from "./kanban/base-kanban-root";
|
||||
import { IssuesListLayoutRoot } from "./list/base-list-root";
|
||||
|
||||
type Props = {
|
||||
peekId: string | undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// plane imports
|
||||
import type { IIssueDisplayProperties } from "@plane/types";
|
||||
|
||||
interface IWithDisplayPropertiesHOC {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
|
||||
@@ -4,17 +4,21 @@ 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 { useIssueFilter, useIssueDetails } from "@/hooks/store";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
// types
|
||||
import { TIssueLayout } from "@/types/issue";
|
||||
import type { TIssueLayout } from "@/types/issue";
|
||||
// local imports
|
||||
import { IssuesLayoutSelection } from "./layout-selection";
|
||||
import { NavbarTheme } from "./theme";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
|
||||
export type NavbarControlsProps = {
|
||||
publishSettings: PublishStore;
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
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";
|
||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||
// mobx
|
||||
import { TIssueLayout } from "@/types/issue";
|
||||
import { IssueLayoutIcon } from "./layout-icon";
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Briefcase } from "lucide-react";
|
||||
// components
|
||||
import { ProjectLogo } from "@/components/common";
|
||||
import { NavbarControls } from "@/components/issues";
|
||||
import { ProjectLogo } from "@/components/common/project-logo";
|
||||
// store
|
||||
import { PublishStore } from "@/store/publish/publish.store";
|
||||
import type { PublishStore } from "@/store/publish/publish.store";
|
||||
// local imports
|
||||
import { NavbarControls } from "./controls";
|
||||
|
||||
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";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// editor components
|
||||
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
// services
|
||||
const fileService = new SitesFileService();
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import { EditorRefApi } from "@plane/editor";
|
||||
import { TIssuePublicComment } from "@plane/types";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { LiteTextEditor } from "@/components/editor";
|
||||
import { CommentReactions } from "@/components/issues/peek-overview";
|
||||
import { LiteTextEditor } from "@/components/editor/lite-text-editor";
|
||||
import { CommentReactions } from "@/components/issues/peek-overview/comment/comment-reactions";
|
||||
// helpers
|
||||
import { timeAgo } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -12,7 +12,8 @@ import { ReactionSelector } from "@/components/ui";
|
||||
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { queryParamGenerator } from "@/helpers/query-param-generator";
|
||||
// hooks
|
||||
import { useIssueDetails, useUser } from "@/hooks/store";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./add-comment";
|
||||
export * from "./comment-detail-card";
|
||||
export * from "./comment-reactions";
|
||||
@@ -1,16 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
// plane imports
|
||||
import { Loader } from "@plane/ui";
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
} from "@/components/issues/peek-overview";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
import type { IIssue } from "@/types/issue";
|
||||
// local imports
|
||||
import { PeekOverviewHeader } from "./header";
|
||||
import { PeekOverviewIssueActivity } from "./issue-activity";
|
||||
import { PeekOverviewIssueDetails } from "./issue-details";
|
||||
import { PeekOverviewIssueProperties } from "./issue-properties";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Link2, MoveRight } from "lucide-react";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui";
|
||||
import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import useClipboardWritePermission from "@/hooks/use-clipboard-write-permission";
|
||||
// types
|
||||
import { IIssue, IPeekMode } from "@/types/issue";
|
||||
@@ -65,29 +66,49 @@ export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
|
||||
<MoveRight className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<CustomSelect
|
||||
<Listbox
|
||||
as="div"
|
||||
value={peekMode}
|
||||
onChange={(val: any) => setPeekMode(val)}
|
||||
onChange={(val) => setPeekMode(val)}
|
||||
className="relative flex-shrink-0 text-left"
|
||||
customButtonClassName={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`}
|
||||
customButton={<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />}
|
||||
optionsClassName="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none"
|
||||
>
|
||||
<div className="space-y-1 p-2">
|
||||
{PEEK_MODES.map((mode) => (
|
||||
<CustomSelect.Option
|
||||
key={mode.key}
|
||||
value={mode.key}
|
||||
className="cursor-pointer select-none truncate rounded px-1 py-1.5"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.label}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</div>
|
||||
</CustomSelect>
|
||||
<Listbox.Button
|
||||
className={`grid place-items-center text-custom-text-300 hover:text-custom-text-200 ${peekMode === "full" ? "rotate-45" : ""}`}
|
||||
>
|
||||
<Icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Listbox.Options className="absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-90 text-xs shadow-lg focus:outline-none">
|
||||
<div className="space-y-1 p-2">
|
||||
{PEEK_MODES.map((mode) => (
|
||||
<Listbox.Option
|
||||
key={mode.key}
|
||||
value={mode.key}
|
||||
className={({ active, selected }) =>
|
||||
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
|
||||
{mode.label}
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
</div>
|
||||
{isClipboardWriteAllowed && (peekMode === "side" || peekMode === "modal") && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
export * from "./comment";
|
||||
export * from "./full-screen-peek-view";
|
||||
export * from "./header";
|
||||
export * from "./issue-activity";
|
||||
export * from "./issue-details";
|
||||
export * from "./issue-properties";
|
||||
export * from "./layout";
|
||||
export * from "./side-peek-view";
|
||||
export * from "./issue-reaction";
|
||||
|
||||
@@ -4,12 +4,16 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { CommentCard, AddComment } from "@/components/issues/peek-overview";
|
||||
import { AddComment } from "@/components/issues/peek-overview/comment/add-comment";
|
||||
import { CommentCard } from "@/components/issues/peek-overview/comment/comment-detail-card";
|
||||
import { Icon } from "@/components/ui";
|
||||
// hooks
|
||||
import { useIssueDetails, usePublish, useUser } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { RichTextEditor } from "@/components/editor";
|
||||
import { IssueReactions } from "@/components/issues/peek-overview";
|
||||
import { usePublish } from "@/hooks/store";
|
||||
// plane imports
|
||||
import { RichTextEditor } from "@/components/editor/rich-text-editor";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
// local imports
|
||||
import { IssueReactions } from "./issue-reaction";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
|
||||
@@ -14,7 +14,8 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { copyTextToClipboard, addSpaceIfCamelCase } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { usePublish, useStates } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
// types
|
||||
import { IIssue, IPeekMode } from "@/types/issue";
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { IssueEmojiReactions, IssueVotes } from "@/components/issues/reactions";
|
||||
// components
|
||||
import { IssueEmojiReactions } from "@/components/issues/reactions/issue-emoji-reactions";
|
||||
import { IssueVotes } from "@/components/issues/reactions/issue-vote-reactions";
|
||||
// hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
import useIsInIframe from "@/hooks/use-is-in-iframe";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FC, Fragment, useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// components
|
||||
import { FullScreenPeekView, SidePeekView } from "@/components/issues/peek-overview";
|
||||
// hooks
|
||||
import { useIssueDetails } from "@/hooks/store";
|
||||
import { useIssueDetails } from "@/hooks/store/use-issue-details";
|
||||
// local imports
|
||||
import { FullScreenPeekView } from "./full-screen-peek-view";
|
||||
import { SidePeekView } from "./side-peek-view";
|
||||
|
||||
type TIssuePeekOverview = {
|
||||
anchor: string;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
// plane imports
|
||||
import { Loader } from "@plane/ui";
|
||||
import {
|
||||
PeekOverviewHeader,
|
||||
PeekOverviewIssueActivity,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewIssueProperties,
|
||||
} from "@/components/issues/peek-overview";
|
||||
// store hooks
|
||||
import { usePublish } from "@/hooks/store";
|
||||
import { usePublish } from "@/hooks/store/publish";
|
||||
// types
|
||||
import { IIssue } from "@/types/issue";
|
||||
// local imports
|
||||
import { PeekOverviewHeader } from "./header";
|
||||
import { PeekOverviewIssueActivity } from "./issue-activity";
|
||||
import { PeekOverviewIssueDetails } from "./issue-details";
|
||||
import { PeekOverviewIssueProperties } from "./issue-properties";
|
||||
|
||||
type Props = {
|
||||
anchor: string;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./issue-emoji-reactions";
|
||||
export * from "./issue-vote-reactions";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user