Compare commits

..
Author SHA1 Message Date
sangeethailango b4acaa2086 chore: test cases 2025-09-04 17:04:06 +05:30
sangeethailango 00e38a7149 chore: move validation to serializer 2025-09-03 16:40:17 +05:30
sangeethailango 04e14f32bf chore: move validation to viewset 2025-09-03 16:21:41 +05:30
gakshita 56795bbcba fix: allowed empty screen for last name 2025-09-03 16:14:10 +05:30
gakshita 03bbe79320 Merge branch 'fix-user-name-validation' of https://github.com/makeplane/plane into fix-user-name-validation 2025-08-28 19:21:59 +05:30
gakshita 9ef0de4848 fix: allowed spaces 2025-08-28 19:21:57 +05:30
sangeethailango 4e4e3ef659 Merge branch 'fix-user-name-validation' of github.com:makeplane/plane into fix-user-name-validation 2025-08-28 19:18:23 +05:30
sangeethailango 43cdaec895 chore: allow space 2025-08-28 19:15:09 +05:30
gakshita fde6de3fcf fix: handled fe to not allow special characters in first_name and last_name 2025-08-28 16:04:50 +05:30
sangeethailango 33d3e032b3 fix: user name validation 2025-08-21 14:57:59 +05:30
Anmol Singh BhatiaandGitHub 03479cf6b3 [WEB-4684] chore: dialog component enhancements (#7606)
* chore: z-index tokens added

* chore: dialog component code refactor

* chore: dialog component improvements

* fix: lint error

* fix: lint error

* fix: format error
2025-08-20 22:17:26 +05:30
Bavisetti NarayanandGitHub b8a88fe89c [WIKI-599] chore: removed the regex tags calculation in description (#7608) 2025-08-20 21:26:21 +05:30
Anmol Singh BhatiaandGitHub 409ac30c91 [WEB-4683] feat: popover component added to propel package #7607 2025-08-20 20:50:27 +05:30
Bavisetti NarayanandGitHub a59ebadd34 [WEB-4712] chore: work item attachment patch endpoint (#7595) 2025-08-20 18:56:15 +05:30
Akshita GoyalandGitHub 174ebfad56 [WEB-4637] fix: scrolling issue fixed #7600 2025-08-20 18:55:22 +05:30
SangeethaandGitHub 008e048968 [WEB-4430] fix: incorrect WI count while scrolling (#7596)
* fix: wrong WI count while scrolling

* chore: optimize issue queryset

* fix: use separate query for total_count_queryset

* fix: guest visibility constraint

* fix: use separate query for total_count_queryset in external api

* fix: use queryset.count()
2025-08-20 18:54:32 +05:30
sriram veeraghantaandGitHub 7e15fcc080 fix: docker node_modeles symlink path matching with pnpm path (#7605) 2025-08-20 16:17:35 +05:30
sriram veeraghantaandGitHub 6636b8882f fix: package cleanup (#7602) 2025-08-20 02:24:12 +05:30
Bavisetti NarayanandGitHub 6398fc3cba [WEB-4716] chore: created new description model (#7597)
* chore: created new description model

* chore: added project field

* chore: removed the duplicate workspace

* chore: updated the comment
2025-08-20 01:07:23 +05:30
Vamsi KrishnaandGitHub fc698bd9b4 [WEB-4710]feat: added module filters to local storage (#7598)
* feat: added module filters to local storage

* chore: removed debounce
2025-08-20 01:04:17 +05:30
Jayash TripathyandGitHub cd61e8dd44 [WEB-4705] chore: url utilities (#7589)
* feat: add truncated link export and URL utility to respective modules

* refactor: replace Link2 with ExternalLink in TruncatedUrl component

* feat: add TruncatedUrl component and update link exports

* fix: export ParsedURL interface for better accessibility in URL utilities

* refactor: remove TruncatedUrl component and update link exports

* fix: update parseURL function to return undefined for invalid URLs

* refactor: rename ParsedURL interface to IParsedURL for consistency

* refactor: rename IParsedURL to IURLComponents and update parsing functions for improved clarity

* refactor: update URL utility functions and improve documentation for clarity

* refactor: add full URL property to IURLComponents interface and update extractURLComponents function

* refactor: rename createURL function to isUrlValid and update its implementation to validate URL strings

* refactor: rename isUrlValid function to getValidURL and update its implementation to return URL object or undefined
2025-08-19 20:09:03 +05:30
Vamsi KrishnaandGitHub cbcdd86569 [WEB-4698]fix: work items modal select/deselect button #7599 2025-08-19 20:07:14 +05:30
553f01fde1 feat: migrate to pnpm from yarn (#7593)
* chore(repo): migrate to pnpm

* chore(repo): cleanup pnpm integration with turbo

* chore(repo): run lint

* chore(repo): cleanup tsconfigs

* chore: align TypeScript to 5.8.3 across monorepo; update pnpm override and catalog; pnpm install to update lockfile

* chore(repo): revert logger.ts changes

* fix: type errors

* fix: build errors

* fix: pnpm home setup in dockerfiles

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
2025-08-19 20:06:42 +05:30
d8f58d28ed fix: CI to include lint and format along with build (#7482)
* fix(lint): get ci passing again

* chore(ci): run lint before build

* chore(ci): exclude web app from build check for now

The web app takes too long and causes CI to timeout. Once we
improve we will reintroduce.

* fix: formating of files

* fix: adding format to ci

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-08-18 21:27:16 +05:30
b194089fec [WEB-3855] fix: Turkish language support (#7383)
* Türkçe dil desteği , İgilizce okunuşlardan gerçek karşılığına çevirildi.

* "sidebar.intake"="Talep" olarak değiştirildi.

---------

Co-authored-by: Bektaş IŞIK <bektas.isik@aurorabilisim.com>
2025-08-18 21:17:06 +05:30
sriram veeraghantaandGitHub 927da438c7 [PRIME-17] fix: enable github api to fetch latest version information (#7548)
* fix: enable github api to fetch latest version information

* chore: typo fixes

* chore: add timeout to request
2025-08-18 20:12:48 +05:30
sriram veeraghantaandGitHub 9c21fd320c feat: adding baseui components to propel package (#7585)
* feat: adding baseui components

* fix: export from the package.json
2025-08-18 19:35:34 +05:30
Anmol Singh BhatiaandGitHub f142266bed [WEB-4699] chore: loading spinner theme #7587 2025-08-18 18:30:51 +05:30
sriram veeraghantaandGitHub 4b06bc4d2d fix: remove page title hook (#7583)
* fix: page title hook removed

* fix: build errors
2025-08-16 14:10:10 +05:30
Aaryan KhandelwalandGitHub d692db47b2 refactor: space app barrel exports (#7573)
* refactor: space app barrel files

* chore: rename views layout
2025-08-15 13:12:36 +05:30
3391e8580c refactor: remove barrel exports from web app (#7577)
* refactor: remove barrel exports from some compoennt modules

* refactor: remove barrel exports from issue components

* refactor: remove barrel exports from page components

* chore: update type improts

* refactor: remove barrel exports from cycle components

* refactor: remove barrel exports from dropdown components

* refactor: remove barrel exports from ce  components

* refactor: remove barrel exports from some more components

* refactor: remove barrel exports from profile and sidebar components

* chore: update type imports

* refactor: remove barrel exports from store hooks

* chore: dynamically load sticky editor

* fix: lint

* chore: revert sticky dynamic import

* refactor: remove barrel exports from ce issue components

* refactor: remove barrel exports from ce issue components

* refactor: remove barrel exports from ce issue components

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-08-15 13:10:26 +05:30
1344 changed files with 20424 additions and 15140 deletions
+45
View File
@@ -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
View File
@@ -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/
+37
View File
@@ -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
View File
@@ -1 +0,0 @@
nodeLinker: node-modules
+1 -1
View File
@@ -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 -2
View File
@@ -2,5 +2,5 @@
.vercel
.tubro
out/
dis/
build/
dist/
build/
+14 -5
View File
@@ -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"]
+3 -3
View File
@@ -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
View File
@@ -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",
+3 -1
View File
@@ -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",
),
]
+84 -3
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,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
)
+15
View File
@@ -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:
+39 -17
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
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
),
+31 -17
View File
@@ -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",),
},
),
]
+2
View File
@@ -83,3 +83,5 @@ from .label import Label
from .device import Device, DeviceSession
from .sticky import Sticky
from .description import Description, DescriptionVersion
+56
View File
@@ -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()
-6
View File
@@ -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 (_)"
)
-18
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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"]
+17 -7
View File
@@ -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"]
+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": "*",
"@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 -2
View File
@@ -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;
-2
View File
@@ -21,8 +21,6 @@
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
+3 -2
View File
@@ -2,5 +2,6 @@
.vercel
.tubro
out/
dis/
build/
dist/
build/
node_modules/
+8 -5
View File
@@ -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"]
+14 -5
View File
@@ -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 -2
View File
@@ -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: {
+3 -3
View File
@@ -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();
+10 -7
View File
@@ -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>
);
};
+5 -4
View File
@@ -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;
+4 -4
View File
@@ -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 -1
View File
@@ -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