Compare commits
39 Commits
fix-webhook
...
v0.19-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| da957e06b6 | |||
| c96225c812 | |||
| 527ecd7d22 | |||
| acd8f8d2d6 | |||
| f4cc103238 | |||
| 89f2f87b97 | |||
| eed047bfe3 | |||
| 8cf4260219 | |||
| 1b55411919 | |||
| dc5edca34d | |||
| 2884b0e22e | |||
| 091fdb89ac | |||
| a678844073 | |||
| 4cb5c4d9df | |||
| 45c9dfb3cf | |||
| 4c78cd7c7e | |||
| c4229c9d55 | |||
| 42c4c46939 | |||
| 6918393b63 | |||
| 6196c750f1 | |||
| ed6dd37043 | |||
| efa3eda85e | |||
| eb0877a3c8 | |||
| 73fd6e641c | |||
| 34dd19cb00 | |||
| ecc277c571 | |||
| d69f025b9a | |||
| ed4a0518fc | |||
| 2e2747c1f9 | |||
| a0b9596cb4 | |||
| f71e8a3a0f | |||
| 002fb4547b | |||
| c1b1ba35c1 | |||
| 4566d6e80c | |||
| e8d359e625 | |||
| 351eba8d61 | |||
| 1e27e37b51 | |||
| 7df2e9cf11 | |||
| c6e3f1b932 |
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.18.0"
|
||||
"version": "0.19.0"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ from plane.api.serializers import (
|
||||
LabelSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkspaceEntityPermission,
|
||||
ProjectEntityPermission,
|
||||
ProjectLitePermission,
|
||||
ProjectMemberPermission,
|
||||
|
||||
@@ -38,7 +38,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@@ -191,6 +191,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id, cycle_id):
|
||||
|
||||
@@ -47,7 +47,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
class IssueArchiveViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
@@ -239,6 +239,11 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issue_queryset, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
|
||||
@@ -50,6 +50,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
||||
@@ -241,6 +242,10 @@ class IssueListEndpoint(BaseAPIView):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -440,6 +445,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issue_queryset, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
@@ -503,6 +512,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||
)
|
||||
.first()
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issue = user_timezone_converter(
|
||||
issue, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ from plane.db.models import (
|
||||
Project,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
@@ -229,6 +230,10 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issue_queryset, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
|
||||
@@ -31,6 +31,7 @@ from plane.db.models import (
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@@ -132,6 +133,10 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
sub_issues = user_timezone_converter(
|
||||
sub_issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"sub_issues": sub_issues,
|
||||
|
||||
@@ -32,6 +32,8 @@ from plane.db.models import (
|
||||
ModuleLink,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView
|
||||
@@ -199,6 +201,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"updated_at",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
modules = user_timezone_converter(
|
||||
modules, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
else:
|
||||
queryset = (
|
||||
|
||||
@@ -48,6 +48,8 @@ from plane.db.models import (
|
||||
Project,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
||||
@@ -236,6 +238,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
"updated_at",
|
||||
)
|
||||
).first()
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
module, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(module, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -277,6 +283,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
modules = user_timezone_converter(
|
||||
modules, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
@@ -454,6 +464,10 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).first()
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
module, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(module, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = ModuleIssueSerializer
|
||||
@@ -150,6 +150,11 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
# create multiple issues inside a module
|
||||
|
||||
@@ -185,7 +185,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
.annotate(
|
||||
total_issues=Issue.issue_objects.filter(
|
||||
project_id=self.kwargs.get("pk"),
|
||||
parent__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -204,7 +203,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
archived_issues=Issue.objects.filter(
|
||||
project_id=self.kwargs.get("pk"),
|
||||
archived_at__isnull=False,
|
||||
parent__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
@@ -224,7 +222,6 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||
draft_issues=Issue.objects.filter(
|
||||
project_id=self.kwargs.get("pk"),
|
||||
is_draft=True,
|
||||
parent__isnull=True,
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
|
||||
@@ -42,7 +42,7 @@ from plane.db.models import (
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||
|
||||
class GlobalViewViewSet(BaseViewSet):
|
||||
serializer_class = IssueViewSerializer
|
||||
@@ -255,6 +255,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.filter(archived_at__isnull=False)
|
||||
.filter(archived_at__isnull=True)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
|
||||
@@ -30,7 +30,7 @@ class WorkspaceModulesEndpoint(BaseAPIView):
|
||||
.select_related("workspace")
|
||||
.select_related("lead")
|
||||
.prefetch_related("members")
|
||||
.filter(archived_at__isnull=False)
|
||||
.filter(archived_at__isnull=True)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"link_module",
|
||||
|
||||
@@ -46,7 +46,7 @@ class Command(BaseCommand):
|
||||
}
|
||||
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Free",
|
||||
instance_name="Plane Community Edition",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytz
|
||||
|
||||
def user_timezone_converter(queryset, datetime_fields, user_timezone):
|
||||
# Create a timezone object for the user's timezone
|
||||
user_tz = pytz.timezone(user_timezone)
|
||||
|
||||
# Check if queryset is a dictionary (single item) or a list of dictionaries
|
||||
if isinstance(queryset, dict):
|
||||
queryset_values = [queryset]
|
||||
else:
|
||||
queryset_values = list(queryset.values())
|
||||
|
||||
# Iterate over the dictionaries in the list
|
||||
for item in queryset_values:
|
||||
# Iterate over the datetime fields
|
||||
for field in datetime_fields:
|
||||
# Convert the datetime field to the user's timezone
|
||||
if item[field]:
|
||||
item[field] = item[field].astimezone(user_tz)
|
||||
|
||||
# If queryset was a single item, return a single item
|
||||
if isinstance(queryset, dict):
|
||||
return queryset_values[0]
|
||||
else:
|
||||
return queryset_values
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
.ProseMirror {
|
||||
--font-size-h1: 1.5rem;
|
||||
--font-size-h2: 1.3125rem;
|
||||
--font-size-h3: 1.125rem;
|
||||
--font-size-h4: 0.9375rem;
|
||||
--font-size-h5: 0.8125rem;
|
||||
--font-size-h6: 0.75rem;
|
||||
--font-size-regular: 0.9375rem;
|
||||
--font-size-list: var(--font-size-regular);
|
||||
}
|
||||
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
@@ -56,7 +67,7 @@
|
||||
|
||||
/* to-do list */
|
||||
ul[data-type="taskList"] li {
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-list);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -162,7 +173,7 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
cursor: text;
|
||||
line-height: 1.2;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-regular);
|
||||
color: inherit;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
@@ -310,15 +321,15 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-size-h1);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1.4rem;
|
||||
margin-bottom: 1px;
|
||||
font-size: 1.5rem;
|
||||
font-size: var(--font-size-h2);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -326,21 +337,23 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1px;
|
||||
font-size: 1.25rem;
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1px;
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-h4);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1px;
|
||||
font-size: 0.9rem;
|
||||
font-size: var(--font-size-h5);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -348,7 +361,7 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1px;
|
||||
font-size: 0.83rem;
|
||||
font-size: var(--font-size-h6);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -356,14 +369,14 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 1px;
|
||||
padding: 3px 2px;
|
||||
font-size: 1rem;
|
||||
padding: 3px 0;
|
||||
font-size: var(--font-size-regular);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
|
||||
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-list);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/document-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Pages Editor",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-extensions",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Editor with extensions",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/lite-text-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Comment Editor",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface ILiteTextEditor {
|
||||
};
|
||||
tabIndex?: number;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||
@@ -48,12 +49,14 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||
tabIndex,
|
||||
mentionHandler,
|
||||
placeholder = "Add comment...",
|
||||
id = "",
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
onChange,
|
||||
initialValue,
|
||||
value,
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/rich-text-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Rich Text Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -20,7 +20,7 @@ export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
|
||||
export type TInboxIssueStatus = EInboxIssueStatus;
|
||||
|
||||
// filters
|
||||
export type TInboxIssueFilterMemberKeys = "assignee" | "created_by";
|
||||
export type TInboxIssueFilterMemberKeys = "assignees" | "created_by";
|
||||
|
||||
export type TInboxIssueFilterDateKeys = "created_at" | "updated_at";
|
||||
|
||||
|
||||
Vendored
-5
@@ -16,14 +16,9 @@ export type TPage = {
|
||||
project: string | undefined;
|
||||
updated_at: Date | undefined;
|
||||
updated_by: string | undefined;
|
||||
view_props: TPageViewProps | undefined;
|
||||
workspace: string | undefined;
|
||||
};
|
||||
|
||||
export type TPageViewProps = {
|
||||
full_width?: boolean;
|
||||
};
|
||||
|
||||
// page filters
|
||||
export type TPageNavigationTabs = "public" | "private" | "archived";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
||||
mentionHandler={{ highlights: mentionHighlights }}
|
||||
{...props}
|
||||
// overriding the customClassName to add relative class passed
|
||||
containerClassName={cn(props.containerClassName, "relative border border-custom-border-200 p-3")}
|
||||
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
setIsSearching(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, workspaceSlug]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -180,6 +180,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -33,7 +33,7 @@ const CollaboratorListItem: React.FC<CollaboratorListItemProps> = observer((prop
|
||||
<div className="flex justify-center">
|
||||
<Avatar
|
||||
src={userDetails.avatar}
|
||||
name={isCurrentUser ? "You" : userDetails.display_name}
|
||||
name={userDetails.display_name}
|
||||
size={69}
|
||||
className="!text-3xl !font-medium"
|
||||
showTooltip={false}
|
||||
|
||||
@@ -51,7 +51,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
suggestions: mentionSuggestions,
|
||||
}}
|
||||
{...rest}
|
||||
containerClassName={cn(containerClassName, "relative min-h-[150px] border border-custom-border-200 p-3")}
|
||||
containerClassName={cn("relative pl-3", containerClassName)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
||||
}}
|
||||
{...props}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(props.containerClassName, "relative border border-custom-border-200 p-3")}
|
||||
containerClassName={cn(props.containerClassName, "relative pl-3")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
|
||||
"fixed inset-0 z-20 bg-custom-background-100": fullScreenMode,
|
||||
"border-[0.5px] border-custom-border-200": border,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -147,8 +147,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const issueCount = cycleDetails
|
||||
? issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues
|
||||
? cycleDetails.total_issues + cycleDetails?.sub_issues
|
||||
? !issueFilters?.displayFilters?.sub_issue && cycleDetails?.sub_issues
|
||||
? cycleDetails.total_issues - cycleDetails?.sub_issues
|
||||
: cycleDetails.total_issues
|
||||
: undefined;
|
||||
|
||||
@@ -227,9 +227,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
className="ml-1.5 flex-shrink-0 truncate"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{currentProjectCycleIds?.map((cycleId) => (
|
||||
<CycleDropdownOption key={cycleId} cycleId={cycleId} />
|
||||
))}
|
||||
{currentProjectCycleIds?.map((cycleId) => <CycleDropdownOption key={cycleId} cycleId={cycleId} />)}
|
||||
</CustomMenu>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -147,8 +147,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const issueCount = moduleDetails
|
||||
? issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues
|
||||
? moduleDetails.total_issues + moduleDetails.sub_issues
|
||||
? !issueFilters?.displayFilters?.sub_issue && moduleDetails.sub_issues
|
||||
? moduleDetails.total_issues - moduleDetails.sub_issues
|
||||
: moduleDetails.total_issues
|
||||
: undefined;
|
||||
|
||||
@@ -227,9 +227,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
className="ml-1.5 flex-shrink-0"
|
||||
placement="bottom-start"
|
||||
>
|
||||
{projectModuleIds?.map((moduleId) => (
|
||||
<ModuleDropdownOption key={moduleId} moduleId={moduleId} />
|
||||
))}
|
||||
{projectModuleIds?.map((moduleId) => <ModuleDropdownOption key={moduleId} moduleId={moduleId} />)}
|
||||
</CustomMenu>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -27,8 +27,8 @@ export const ProjectArchivesHeader: FC = observer(() => {
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const issueCount = currentProjectDetails
|
||||
? issueFilters?.displayFilters?.sub_issue
|
||||
? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues
|
||||
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails.archived_sub_issues
|
||||
? currentProjectDetails.archived_issues - currentProjectDetails.archived_sub_issues
|
||||
: currentProjectDetails.archived_issues
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
);
|
||||
|
||||
const issueCount = currentProjectDetails
|
||||
? issueFilters?.displayFilters?.sub_issue
|
||||
? currentProjectDetails.draft_issues + currentProjectDetails.draft_sub_issues
|
||||
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails.draft_sub_issues
|
||||
? currentProjectDetails.draft_issues - currentProjectDetails.draft_sub_issues
|
||||
: currentProjectDetails.draft_issues
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
|
||||
|
||||
const issueCount = currentProjectDetails
|
||||
? issueFilters?.displayFilters?.sub_issue
|
||||
? currentProjectDetails?.total_issues + currentProjectDetails?.sub_issues
|
||||
? !issueFilters?.displayFilters?.sub_issue && currentProjectDetails?.sub_issues
|
||||
? currentProjectDetails?.total_issues - currentProjectDetails?.sub_issues
|
||||
: currentProjectDetails?.total_issues
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -292,32 +292,36 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
</ControlLink>
|
||||
</div>
|
||||
) : (
|
||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||
{canMarkAsAccepted && (
|
||||
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
Snooze
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<>
|
||||
{isAllowed && (
|
||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||
{canMarkAsAccepted && (
|
||||
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
Snooze
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canMarkAsDuplicate && (
|
||||
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileStack size={14} strokeWidth={2} />
|
||||
Mark as duplicate
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canDelete && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
{canMarkAsDuplicate && (
|
||||
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileStack size={14} strokeWidth={2} />
|
||||
Mark as duplicate
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canDelete && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +31,10 @@ export const InboxIssueContentProperties: React.FC<Props> = observer((props) =>
|
||||
const minDate = issue.start_date ? getDate(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
if (!issue || !issue?.id) return <></>;
|
||||
|
||||
return (
|
||||
<div className="flex h-min w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="h-min w-full overflow-y-auto px-5">
|
||||
<div className="h-min w-full overflow-y-auto px-3">
|
||||
<h5 className="text-sm font-medium my-4">Properties</h5>
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!isEditable ? "opacity-60" : ""}`}>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -114,7 +114,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
<div className="rounded-lg space-y-4 pl-3">
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
@@ -124,6 +124,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
issueOperations={issueOperations}
|
||||
disabled={!isEditable}
|
||||
value={issue.name}
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
{loader === "issue-loading" ? (
|
||||
@@ -135,11 +136,12 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
issueId={issue.id}
|
||||
swrIssueDescription={null}
|
||||
swrIssueDescription={issue.description_html ?? "<p></p>"}
|
||||
initialValue={issue.description_html ?? "<p></p>"}
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -152,12 +154,15 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IssueAttachmentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
|
||||
<div className="pl-3">
|
||||
<IssueAttachmentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issue.id}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InboxIssueContentProperties
|
||||
workspaceSlug={workspaceSlug}
|
||||
@@ -168,7 +173,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
duplicateIssueDetails={inboxIssue?.duplicate_issue_detail}
|
||||
/>
|
||||
|
||||
<div className="pb-12">
|
||||
<div className="pb-12 pl-3">
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issue.id} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5 vertical-scrollbar scrollbar-md">
|
||||
<div className="h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5 vertical-scrollbar scrollbar-md">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
||||
@@ -34,8 +34,8 @@ export const InboxIssueAppliedFiltersMember: FC<InboxIssueAppliedFiltersMember>
|
||||
if (!optionDetail) return <></>;
|
||||
return (
|
||||
<div key={value} className="relative flex items-center gap-1 rounded bg-custom-background-80 p-1 text-xs">
|
||||
<div className="w-3 h-3 flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
||||
<Avatar name={optionDetail.display_name} src={optionDetail.avatar} showTooltip={false} size="md" />
|
||||
<div className="flex-shrink-0 relative flex justify-center items-center overflow-hidden">
|
||||
<Avatar name={optionDetail.display_name} src={optionDetail.avatar} showTooltip={false} size="sm" />
|
||||
</div>
|
||||
<div className="text-xs truncate">{optionDetail?.display_name}</div>
|
||||
<div
|
||||
|
||||
@@ -25,7 +25,7 @@ export const InboxIssueAppliedFilters: FC = observer(() => {
|
||||
{/* priority */}
|
||||
<InboxIssueAppliedFiltersPriority />
|
||||
{/* assignees */}
|
||||
<InboxIssueAppliedFiltersMember filterKey="assignee" label="Assignee" />
|
||||
<InboxIssueAppliedFiltersMember filterKey="assignees" label="Assignees" />
|
||||
{/* created_by */}
|
||||
<InboxIssueAppliedFiltersMember filterKey="created_by" label="Created By" />
|
||||
{/* label */}
|
||||
|
||||
@@ -60,8 +60,8 @@ export const InboxIssueFilterSelection: FC = observer(() => {
|
||||
{/* assignees */}
|
||||
<div className="py-2">
|
||||
<FilterMember
|
||||
filterKey="assignee"
|
||||
label="Assignee"
|
||||
filterKey="assignees"
|
||||
label="Assignees"
|
||||
searchQuery={filtersSearchQuery}
|
||||
memberIds={projectMemberIds ?? []}
|
||||
/>
|
||||
|
||||
@@ -133,6 +133,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
<div className="relative flex justify-between items-center gap-3">
|
||||
|
||||
@@ -138,6 +138,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
<div className="relative flex justify-end items-center gap-3">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||
import { useProjectInbox } from "@/hooks/store";
|
||||
|
||||
type TInboxIssueDescription = {
|
||||
containerClassName?: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
workspaceId: string;
|
||||
@@ -21,7 +22,7 @@ type TInboxIssueDescription = {
|
||||
|
||||
// TODO: have to implement GPT Assistance
|
||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||
const { workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
// hooks
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
@@ -42,6 +43,7 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Sparkle } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
import { EditorRefApi } from "@plane/rich-text-editor";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { GptAssistantPopover } from "@/components/core";
|
||||
import { PriorityDropdown } from "@/components/dropdowns";
|
||||
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
|
||||
import { ISSUE_CREATED } from "@/constants/event-tracker";
|
||||
import { useApplication, useEventTracker, useWorkspace, useProjectInbox } from "@/hooks/store";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<TIssue> = {
|
||||
name: "",
|
||||
description_html: "<p></p>",
|
||||
priority: "none",
|
||||
};
|
||||
|
||||
// services
|
||||
const aiService = new AIService();
|
||||
|
||||
export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
if (!workspaceSlug || !projectId) return null;
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
const [gptAssistantModal, setGptAssistantModal] = useState(false);
|
||||
const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false);
|
||||
// refs
|
||||
const editorRef = useRef<EditorRefApi>(null);
|
||||
// hooks
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug.toString() as string)?.id.toString() as string;
|
||||
|
||||
// store hooks
|
||||
const { createInboxIssue } = useProjectInbox();
|
||||
const {
|
||||
config: { envConfig },
|
||||
} = useApplication();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
getValues,
|
||||
} = useForm<Partial<TIssue>>({ defaultValues });
|
||||
const issueName = watch("name");
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
reset(defaultValues);
|
||||
editorRef?.current?.clearEditor();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
await createInboxIssue(workspaceSlug.toString(), projectId.toString(), formData)
|
||||
.then((res) => {
|
||||
if (!createMore) {
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/inbox/?currentTab=open&inboxIssueId=${res?.issue?.id}`);
|
||||
handleClose();
|
||||
} else {
|
||||
reset(defaultValues);
|
||||
editorRef?.current?.clearEditor();
|
||||
}
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "SUCCESS",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_CREATED,
|
||||
payload: {
|
||||
...formData,
|
||||
state: "FAILED",
|
||||
element: "Inbox page",
|
||||
},
|
||||
path: router.pathname,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleAiAssistance = async (response: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
editorRef.current?.setEditorValueAtCursorPosition(response);
|
||||
};
|
||||
|
||||
const handleAutoGenerateDescription = async () => {
|
||||
const issueName = getValues("name");
|
||||
if (!workspaceSlug || !projectId || !issueName) return;
|
||||
|
||||
setIAmFeelingLucky(true);
|
||||
|
||||
aiService
|
||||
.createGptTask(workspaceSlug as string, projectId as string, {
|
||||
prompt: issueName,
|
||||
task: "Generate a proper description for this issue.",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.response === "")
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message:
|
||||
"Issue title isn't informative enough to generate the description. Please try with a different title.",
|
||||
});
|
||||
else handleAiAssistance(res.response_html);
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.data?.error;
|
||||
|
||||
if (err.status === 429)
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error || "You have reached the maximum number of requests of 50 requests per month per user.",
|
||||
});
|
||||
else
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error || "Some error occurred. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIAmFeelingLucky(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div className="space-y-5">
|
||||
<h3 className="text-xl font-semibold leading-6 text-custom-text-100">Create Inbox Issue</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="mt-2 space-y-3">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange, ref } }) => (
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full resize-none text-xl"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="border-0.5 absolute bottom-3.5 right-3.5 z-10 flex rounded bg-custom-background-80">
|
||||
{watch("name") && issueName !== "" && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90 ${
|
||||
iAmFeelingLucky ? "cursor-wait" : ""
|
||||
}`}
|
||||
onClick={handleAutoGenerateDescription}
|
||||
disabled={iAmFeelingLucky}
|
||||
>
|
||||
{iAmFeelingLucky ? (
|
||||
"Generating response..."
|
||||
) : (
|
||||
<>
|
||||
<Sparkle className="h-4 w-4" />I{"'"}m feeling lucky
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{envConfig?.has_openai_configured && (
|
||||
<GptAssistantPopover
|
||||
isOpen={gptAssistantModal}
|
||||
projectId={projectId.toString()}
|
||||
handleClose={() => {
|
||||
setGptAssistantModal((prevData) => !prevData);
|
||||
// this is done so that the title do not reset after gpt popover closed
|
||||
reset(getValues());
|
||||
}}
|
||||
onResponse={(response) => {
|
||||
handleAiAssistance(response);
|
||||
}}
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
|
||||
onClick={() => setGptAssistantModal((prevData) => !prevData)}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" />
|
||||
AI
|
||||
</button>
|
||||
}
|
||||
className="!min-w-[38rem]"
|
||||
placement="top-end"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Controller
|
||||
name="description_html"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RichTextEditor
|
||||
initialValue={!value || value === "" ? "<p></p>" : value}
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId.toString()}
|
||||
dragDropEnabled={false}
|
||||
onChange={(_description: object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="h-5">
|
||||
<PriorityDropdown
|
||||
value={value ?? "none"}
|
||||
onChange={onChange}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mx-5 mt-5 flex items-center justify-between gap-2 border-t border-custom-border-200 px-5 pt-5">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1"
|
||||
onClick={() => setCreateMore((prevData) => !prevData)}
|
||||
>
|
||||
<span className="text-xs">Create more</span>
|
||||
<ToggleSwitch value={createMore} onChange={() => {}} size="md" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={() => handleClose()}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Adding Issue..." : "Add Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
||||
@@ -2,19 +2,19 @@ import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { AlertCircle, X } from "lucide-react";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { getFileIcon } from "@/components/icons/attachment";
|
||||
// icons
|
||||
import { getFileIcon } from "@/components/icons";
|
||||
// components
|
||||
import { IssueAttachmentDeleteModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { convertBytesToSize, getFileExtension, getFileName } from "@/helpers/attachment.helper";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// hooks
|
||||
// ui
|
||||
// components
|
||||
// icons
|
||||
// helper
|
||||
import { IssueAttachmentDeleteModal } from "./delete-attachment-confirmation-modal";
|
||||
// types
|
||||
import { TAttachmentOperations } from "./root";
|
||||
|
||||
@@ -36,24 +36,24 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
isDeleteAttachmentModalOpen,
|
||||
toggleDeleteAttachmentModal,
|
||||
} = useIssueDetail();
|
||||
// states
|
||||
// derived values
|
||||
const attachment = attachmentId ? getAttachmentById(attachmentId) : undefined;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const attachment = attachmentId && getAttachmentById(attachmentId);
|
||||
|
||||
if (!attachment) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={isDeleteAttachmentModalOpen}
|
||||
setIsOpen={() => toggleDeleteAttachmentModal(false)}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
data={attachment}
|
||||
/>
|
||||
|
||||
<div
|
||||
key={attachmentId}
|
||||
className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm"
|
||||
>
|
||||
{isDeleteAttachmentModalOpen === attachment.id && (
|
||||
<IssueAttachmentDeleteModal
|
||||
isOpen={!!isDeleteAttachmentModalOpen}
|
||||
onClose={() => toggleDeleteAttachmentModal(null)}
|
||||
handleAttachmentOperations={handleAttachmentOperations}
|
||||
data={attachment}
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-[60px] items-center justify-between gap-1 rounded-md border-[2px] border-custom-border-200 bg-custom-background-100 px-4 py-2 text-sm">
|
||||
<Link href={attachment.asset} target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-7 w-7">{getFileIcon(getFileExtension(attachment.asset))}</div>
|
||||
@@ -83,7 +83,7 @@ export const IssueAttachmentsDetail: FC<TIssueAttachmentsDetail> = observer((pro
|
||||
</Link>
|
||||
|
||||
{!disabled && (
|
||||
<button onClick={() => toggleDeleteAttachmentModal(true)}>
|
||||
<button type="button" onClick={() => toggleDeleteAttachmentModal(attachment.id)}>
|
||||
<X className="h-4 w-4 text-custom-text-200 hover:text-custom-text-100" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
// hooks
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { generateFileName } from "@/helpers/attachment.helper";
|
||||
// hooks
|
||||
import { useApplication } from "@/hooks/store";
|
||||
// types
|
||||
import { TAttachmentOperations } from "./root";
|
||||
@@ -27,24 +27,28 @@ export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
const currentFile: File = acceptedFiles[0];
|
||||
if (!currentFile || !workspaceSlug) return;
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const currentFile: File = acceptedFiles[0];
|
||||
if (!currentFile || !workspaceSlug) return;
|
||||
|
||||
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), { type: currentFile.type });
|
||||
const formData = new FormData();
|
||||
formData.append("asset", uploadedFile);
|
||||
formData.append(
|
||||
"attributes",
|
||||
JSON.stringify({
|
||||
name: uploadedFile.name,
|
||||
size: uploadedFile.size,
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const uploadedFile: File = new File([currentFile], generateFileName(currentFile.name), {
|
||||
type: currentFile.type,
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append("asset", uploadedFile);
|
||||
formData.append(
|
||||
"attributes",
|
||||
JSON.stringify({
|
||||
name: uploadedFile.name,
|
||||
size: uploadedFile.size,
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
||||
},
|
||||
[handleAttachmentOperations, workspaceSlug]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
|
||||
onDrop,
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
import { FC, Fragment, Dispatch, SetStateAction, useState } from "react";
|
||||
import { FC, Fragment, useState } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import type { TIssueAttachment } from "@plane/types";
|
||||
@@ -14,18 +14,18 @@ export type TAttachmentOperationsRemoveModal = Exclude<TAttachmentOperations, "c
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
onClose: () => void;
|
||||
data: TIssueAttachment;
|
||||
handleAttachmentOperations: TAttachmentOperationsRemoveModal;
|
||||
};
|
||||
|
||||
export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
|
||||
const { isOpen, setIsOpen, data, handleAttachmentOperations } = props;
|
||||
// state
|
||||
const { isOpen, onClose, data, handleAttachmentOperations } = props;
|
||||
// states
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
setLoader(false);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export * from "./root";
|
||||
|
||||
export * from "./attachment-upload";
|
||||
export * from "./delete-attachment-confirmation-modal";
|
||||
|
||||
export * from "./attachments-list";
|
||||
export * from "./attachment-detail";
|
||||
export * from "./attachment-upload";
|
||||
export * from "./attachments-list";
|
||||
export * from "./delete-attachment-modal";
|
||||
export * from "./root";
|
||||
|
||||
@@ -95,7 +95,7 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
||||
}
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
|
||||
[captureIssueEvent, workspaceSlug, projectId, issueId, createAttachment, removeAttachment]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
export type IssueDescriptionInputProps = {
|
||||
containerClassName?: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
@@ -28,6 +29,7 @@ export type IssueDescriptionInputProps = {
|
||||
|
||||
export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((props) => {
|
||||
const {
|
||||
containerClassName,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
@@ -110,11 +112,12 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||
placeholder={
|
||||
placeholder ? placeholder : (isFocused, value) => getDescriptionPlaceholder(isFocused, value)
|
||||
}
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
) : (
|
||||
<RichTextReadOnlyEditor
|
||||
initialValue={localIssueDescription.description_html ?? ""}
|
||||
containerClassName="!p-0 !pt-2 text-custom-text-200 min-h-[150px]"
|
||||
containerClassName={containerClassName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ type TIssueCommentCreate = {
|
||||
workspaceSlug: string;
|
||||
activityOperations: TActivityOperations;
|
||||
showAccessSpecifier?: boolean;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
const { workspaceSlug, projectId, activityOperations, showAccessSpecifier = false } = props;
|
||||
const { workspaceSlug, projectId, issueId, activityOperations, showAccessSpecifier = false } = props;
|
||||
// refs
|
||||
const editorRef = useRef<any>(null);
|
||||
// store hooks
|
||||
@@ -72,6 +73,8 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditor
|
||||
workspaceId={workspaceId}
|
||||
id={"add_comment_" + issueId}
|
||||
value={"<p></p>"}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
onEnterKeyPress={(e) => {
|
||||
|
||||
@@ -146,6 +146,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
@@ -165,6 +166,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
issueId={issueId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg space-y-4">
|
||||
<div className="rounded-lg space-y-4 pl-3">
|
||||
{issue.parent_id && (
|
||||
<IssueParentDetail
|
||||
workspaceSlug={workspaceSlug}
|
||||
@@ -85,6 +85,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
issueOperations={issueOperations}
|
||||
disabled={!isEditable}
|
||||
value={issue.name}
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
{/* {issue?.description_html === issueDescription && ( */}
|
||||
@@ -97,6 +98,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
{/* )} */}
|
||||
|
||||
@@ -121,14 +123,18 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<IssueAttachmentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
<div className="pl-3">
|
||||
<IssueAttachmentRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
|
||||
<div className="pl-3">
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={isArchived} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
try {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||
toggleParentIssueModal(false);
|
||||
toggleParentIssueModal(issueId);
|
||||
} catch (error) {
|
||||
console.error("something went wrong while fetching the issue");
|
||||
}
|
||||
@@ -79,8 +79,8 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
<ParentIssuesListModal
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
isOpen={isParentIssueModalOpen}
|
||||
handleClose={() => toggleParentIssueModal(false)}
|
||||
isOpen={isParentIssueModalOpen === issueId}
|
||||
handleClose={() => toggleParentIssueModal(null)}
|
||||
onChange={(issue: any) => handleParentIssue(issue?.id)}
|
||||
/>
|
||||
<button
|
||||
@@ -94,7 +94,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={() => toggleParentIssueModal(true)}
|
||||
onClick={() => toggleParentIssueModal(issue.id)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{issue.parent_id && parentIssue ? (
|
||||
|
||||
@@ -81,22 +81,26 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
data.map((i) => i.id)
|
||||
);
|
||||
|
||||
toggleRelationModal(null);
|
||||
toggleRelationModal(null, null);
|
||||
};
|
||||
|
||||
if (!relationIssueIds) return null;
|
||||
|
||||
const isRelationKeyModalActive =
|
||||
isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={isRelationModalOpen === relationKey}
|
||||
handleClose={() => toggleRelationModal(null)}
|
||||
isOpen={isRelationKeyModalActive}
|
||||
handleClose={() => toggleRelationModal(null, null)}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
@@ -104,11 +108,11 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
{
|
||||
"cursor-not-allowed": disabled,
|
||||
"hover:bg-custom-background-80": !disabled,
|
||||
"bg-custom-background-80": isRelationModalOpen === relationKey,
|
||||
"bg-custom-background-80": isRelationKeyModalActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={() => toggleRelationModal(relationKey)}
|
||||
onClick={() => toggleRelationModal(issueId, relationKey)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex w-full items-start justify-between">
|
||||
|
||||
@@ -357,7 +357,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full overflow-hidden">
|
||||
<div className="max-w-2/3 h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto p-5">
|
||||
<div className="max-w-2/3 h-full w-full space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto px-6 py-5">
|
||||
<IssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
swrIssueDetails={swrIssueDetails}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
CopyPlus,
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
UserCircle2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
// components
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
} from "@/components/dropdowns";
|
||||
// ui
|
||||
// helpers
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import {
|
||||
DeleteIssueModal,
|
||||
IssueLinkRoot,
|
||||
@@ -56,7 +54,7 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// types
|
||||
import { useEstimate, useIssueDetail, useMember, useProject, useProjectState, useUser } from "@/hooks/store";
|
||||
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import type { TIssueOperations } from "./root";
|
||||
@@ -90,12 +88,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { getUserDetails } = useMember();
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
|
||||
const createdByDetails = getUserDetails(issue.created_by);
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||
@@ -262,21 +257,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{createdByDetails && (
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex h-8 items-center gap-2">
|
||||
<div className="flex w-2/5 flex-shrink-0 items-center gap-1 text-sm text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
|
||||
+7
-6
@@ -96,12 +96,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||
...(appliedFilters ?? {}),
|
||||
},
|
||||
}).then((res) => {
|
||||
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||
view_id: res.id,
|
||||
applied_filters: res.filters,
|
||||
state: "SUCCESS",
|
||||
element: "Spreadsheet view",
|
||||
});
|
||||
if (res)
|
||||
captureEvent(GLOBAL_VIEW_UPDATED, {
|
||||
view_id: res.id,
|
||||
applied_filters: res.filters,
|
||||
state: "SUCCESS",
|
||||
element: "Spreadsheet view",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
const sub_group_by = displayFilters?.sub_group_by;
|
||||
const group_by = displayFilters?.group_by;
|
||||
|
||||
const orderBy = displayFilters?.order_by;
|
||||
|
||||
const userDisplayFilters = displayFilters || null;
|
||||
|
||||
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
|
||||
@@ -157,7 +159,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
issues.getIssueIds,
|
||||
updateIssue,
|
||||
group_by,
|
||||
sub_group_by
|
||||
sub_group_by,
|
||||
orderBy !== "sort_order"
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error",
|
||||
@@ -259,6 +262,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
|
||||
@@ -4,10 +4,11 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||
// hooks
|
||||
import { ControlLink, DropIndicator, Tooltip } from "@plane/ui";
|
||||
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
@@ -131,6 +132,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
|
||||
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
|
||||
|
||||
useOutsideClickDetector(cardRef, () => {
|
||||
cardRef?.current?.classList?.remove("highlight");
|
||||
});
|
||||
|
||||
// Make Issue block both as as Draggable and,
|
||||
// as a DropTarget for other issues being dragged to get the location of drop
|
||||
useEffect(() => {
|
||||
@@ -177,7 +182,15 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
<div
|
||||
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
||||
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
||||
onDragStart={() => isDragAllowed && setIsCurrentBlockDragging(true)}
|
||||
onDragStart={() => {
|
||||
if (isDragAllowed) setIsCurrentBlockDragging(true);
|
||||
else
|
||||
setToast({
|
||||
type: TOAST_TYPE.WARNING,
|
||||
title: "Cannot move issue",
|
||||
message: "Drag and drop is disabled for the current grouping",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
@@ -186,12 +199,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
|
||||
}`}
|
||||
ref={cardRef}
|
||||
className={cn(
|
||||
"block rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{
|
||||
"hover:cursor-pointer": isDragAllowed,
|
||||
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
|
||||
"bg-custom-background-80 z-[100]": isCurrentBlockDragging,
|
||||
}
|
||||
"block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
|
||||
{ "hover:cursor-pointer": isDragAllowed },
|
||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) },
|
||||
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
|
||||
)}
|
||||
target="_blank"
|
||||
onClick={() => handleIssuePeekOverview(issue)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TUnGroupedIssues,
|
||||
TIssueKanbanFilters,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
// hooks
|
||||
@@ -31,6 +32,7 @@ export interface IGroupByKanBan {
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
group_by: TIssueGroupByOptions | undefined;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
sub_group_id: string;
|
||||
isDragDisabled: boolean;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
@@ -79,6 +81,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleOnDrop,
|
||||
showEmptyGroup = true,
|
||||
subGroupIssueHeaderCount,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const member = useMember();
|
||||
@@ -170,6 +173,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
updateIssue={updateIssue}
|
||||
@@ -196,6 +200,7 @@ export interface IKanBan {
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
group_by: TIssueGroupByOptions | undefined;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
sub_group_id?: string;
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: TRenderQuickActions;
|
||||
@@ -242,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleOnDrop,
|
||||
showEmptyGroup,
|
||||
subGroupIssueHeaderCount,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const issueKanBanView = useKanbanView();
|
||||
@@ -253,6 +259,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
orderBy={orderBy}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
|
||||
updateIssue={updateIssue}
|
||||
|
||||
@@ -11,14 +11,21 @@ import {
|
||||
TSubGroupedIssues,
|
||||
TUnGroupedIssues,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
//components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
|
||||
import {
|
||||
KanbanDropLocation,
|
||||
getSourceFromDropPayload,
|
||||
getDestinationFromDropPayload,
|
||||
highlightIssueOnDrop,
|
||||
} from "./utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
|
||||
interface IKanbanGroup {
|
||||
@@ -45,6 +52,7 @@ interface IKanbanGroup {
|
||||
groupByVisibilityToggle?: boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
}
|
||||
|
||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
@@ -52,6 +60,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
groupId,
|
||||
sub_group_id,
|
||||
group_by,
|
||||
orderBy,
|
||||
sub_group_by,
|
||||
issuesMap,
|
||||
displayProperties,
|
||||
@@ -101,13 +110,15 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
if (!source || !destination) return;
|
||||
|
||||
handleOnDrop(source, destination);
|
||||
|
||||
highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order");
|
||||
},
|
||||
}),
|
||||
autoScrollForElements({
|
||||
element,
|
||||
})
|
||||
);
|
||||
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]);
|
||||
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]);
|
||||
|
||||
const prePopulateQuickAddData = (
|
||||
groupByKey: string | undefined,
|
||||
@@ -161,16 +172,33 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
return preloadedData;
|
||||
};
|
||||
|
||||
const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order";
|
||||
const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${groupId}__${sub_group_id}`}
|
||||
className={cn(
|
||||
"relative h-full transition-all min-h-[50px]",
|
||||
{ "bg-custom-background-80": isDraggingOverColumn },
|
||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by }
|
||||
{ "bg-custom-background-80 rounded": isDraggingOverColumn },
|
||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay }
|
||||
)}
|
||||
ref={columnRef}
|
||||
>
|
||||
<div
|
||||
//column overlay when issues are not sorted by manual
|
||||
className={cn(
|
||||
"absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded",
|
||||
{
|
||||
"flex flex-col bg-custom-background-80 border-[1px] border-custom-border-300 z-[2]": shouldOverlay,
|
||||
},
|
||||
{ hidden: !shouldOverlay },
|
||||
{ "justify-center": !sub_group_by }
|
||||
)}
|
||||
>
|
||||
{readableOrderBy && <span className="pt-6">The layout is ordered by {readableOrderBy}.</span>}
|
||||
<span>Drop here to move the issue.</span>
|
||||
</div>
|
||||
<KanbanIssueBlocksList
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={groupId}
|
||||
@@ -181,7 +209,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef}
|
||||
/>
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TUnGroupedIssues,
|
||||
TIssueKanbanFilters,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
@@ -114,6 +115,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||
disableIssueCreation?: boolean;
|
||||
storeType: KanbanStoreType;
|
||||
enableQuickIssueCreate: boolean;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||
quickAddCallback?: (
|
||||
@@ -146,6 +148,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
viewId,
|
||||
scrollableContainerRef,
|
||||
handleOnDrop,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
@@ -181,7 +184,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||
return (
|
||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-[50px] z-[1] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||
<div className="sticky left-0 flex-shrink-0">
|
||||
<HeaderSubGroupByCard
|
||||
column_id={_list.id}
|
||||
@@ -216,6 +219,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
viewId={viewId}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
handleOnDrop={handleOnDrop}
|
||||
orderBy={orderBy}
|
||||
subGroupIssueHeaderCount={(groupByListId: string) =>
|
||||
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
|
||||
}
|
||||
@@ -254,6 +258,7 @@ export interface IKanBanSwimLanes {
|
||||
viewId?: string;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
}
|
||||
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
@@ -263,6 +268,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
displayProperties,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
orderBy,
|
||||
updateIssue,
|
||||
storeType,
|
||||
quickActions,
|
||||
@@ -313,7 +319,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90 px-2">
|
||||
<div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
|
||||
<SubGroupSwimlaneHeader
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
@@ -334,6 +340,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pull from "lodash/pull";
|
||||
import scrollIntoView from "smooth-scroll-into-view-if-needed";
|
||||
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
|
||||
|
||||
@@ -87,14 +88,17 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K
|
||||
const handleSortOrder = (
|
||||
destinationIssues: string[],
|
||||
destinationIssueId: string | undefined,
|
||||
getIssueById: (issueId: string) => TIssue | undefined
|
||||
getIssueById: (issueId: string) => TIssue | undefined,
|
||||
shouldAddIssueAtTop = false
|
||||
) => {
|
||||
const sortOrderDefaultValue = 65535;
|
||||
let currentIssueState = {};
|
||||
|
||||
const destinationIndex = destinationIssueId
|
||||
? destinationIssues.indexOf(destinationIssueId)
|
||||
: destinationIssues.length;
|
||||
: shouldAddIssueAtTop
|
||||
? 0
|
||||
: destinationIssues.length;
|
||||
|
||||
if (destinationIssues && destinationIssues.length > 0) {
|
||||
if (destinationIndex === 0) {
|
||||
@@ -145,7 +149,8 @@ export const handleDragDrop = async (
|
||||
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
|
||||
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
|
||||
groupBy: TIssueGroupByOptions | undefined,
|
||||
subGroupBy: TIssueGroupByOptions | undefined
|
||||
subGroupBy: TIssueGroupByOptions | undefined,
|
||||
shouldAddIssueAtTop = false
|
||||
) => {
|
||||
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||
|
||||
@@ -165,7 +170,7 @@ export const handleDragDrop = async (
|
||||
// for both horizontal and vertical dnd
|
||||
updatedIssue = {
|
||||
...updatedIssue,
|
||||
...handleSortOrder(destinationIssues, destination.id, getIssueById),
|
||||
...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop),
|
||||
};
|
||||
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||
@@ -207,3 +212,18 @@ export const handleDragDrop = async (
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
|
||||
* @param elementId
|
||||
* @param shouldScrollIntoView
|
||||
*/
|
||||
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
|
||||
setTimeout(async () => {
|
||||
const sourceElementId = elementId ?? "";
|
||||
const sourceElement = document.getElementById(sourceElementId);
|
||||
sourceElement?.classList?.add("highlight");
|
||||
if (shouldScrollIntoView && sourceElement)
|
||||
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
|
||||
}, 200);
|
||||
};
|
||||
|
||||
@@ -161,6 +161,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
||||
portalElement={portalElement}
|
||||
placement={placements}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -180,6 +181,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -123,6 +123,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
|
||||
portalElement={portalElement}
|
||||
placement={placements}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -142,6 +143,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -181,6 +181,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -200,6 +201,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -107,6 +107,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
portalElement={portalElement}
|
||||
placement={placements}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -126,6 +127,7 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -178,6 +178,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -197,6 +198,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -171,6 +171,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
@@ -190,6 +191,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -480,6 +480,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
ref={editorRef}
|
||||
tabIndex={getTabIndex("description_html")}
|
||||
placeholder={getDescriptionPlaceholder}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -54,7 +54,7 @@ export type PeekOverviewHeaderProps = {
|
||||
isArchived: boolean;
|
||||
disabled: boolean;
|
||||
toggleDeleteIssueModal: (issueId: string | null) => void;
|
||||
toggleArchiveIssueModal: (value: boolean) => void;
|
||||
toggleArchiveIssueModal: (issueId: string | null) => void;
|
||||
handleRestoreIssue: () => void;
|
||||
isSubmitting: "submitting" | "submitted" | "saved";
|
||||
};
|
||||
@@ -178,7 +178,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isInArchivableGroup) return;
|
||||
toggleArchiveIssueModal(true);
|
||||
toggleArchiveIssueModal(issueId);
|
||||
}}
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
|
||||
@@ -69,6 +69,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
value={issue.name}
|
||||
containerClassName="-ml-3"
|
||||
/>
|
||||
|
||||
<IssueDescriptionInput
|
||||
@@ -77,10 +78,11 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
issueId={issue.id}
|
||||
initialValue={issueDescription}
|
||||
// for now peek overview doesn't have live syncing while tab changes
|
||||
swrIssueDescription={null}
|
||||
swrIssueDescription={issueDescription}
|
||||
disabled={disabled}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
XCircle,
|
||||
CalendarClock,
|
||||
CalendarCheck2,
|
||||
UserCircle2,
|
||||
} from "lucide-react";
|
||||
// hooks
|
||||
// ui icons
|
||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon, Tooltip } from "@plane/ui";
|
||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon, RelatedIcon } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
MemberDropdown,
|
||||
StateDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import {
|
||||
IssueLinkRoot,
|
||||
IssueCycleSelect,
|
||||
@@ -37,8 +35,7 @@ import {
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
workspaceSlug: string;
|
||||
@@ -56,12 +53,9 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
const { getUserDetails } = useMember();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
const createdByDetails = getUserDetails(issue?.created_by);
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
const isEstimateEnabled = projectDetails?.estimate;
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
@@ -137,22 +131,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* created by */}
|
||||
{createdByDetails && (
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Created by</span>
|
||||
</div>
|
||||
<Tooltip tooltipContent={createdByDetails?.display_name} isMobile={isMobile}>
|
||||
<div className="h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-default">
|
||||
<ButtonAvatars showTooltip={false} userIds={createdByDetails?.id} />
|
||||
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* start date */}
|
||||
<div className="flex w-full items-center gap-3 h-8">
|
||||
<div className="flex items-center gap-1 w-1/4 flex-shrink-0 text-sm text-custom-text-300">
|
||||
|
||||
@@ -87,8 +87,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<>
|
||||
{issue && !is_archived && (
|
||||
<ArchiveIssueModal
|
||||
isOpen={isArchiveIssueModalOpen}
|
||||
handleClose={() => toggleArchiveIssueModal(false)}
|
||||
isOpen={isArchiveIssueModalOpen === issueId}
|
||||
handleClose={() => toggleArchiveIssueModal(null)}
|
||||
data={issue}
|
||||
onSubmit={async () => {
|
||||
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
|
||||
|
||||
@@ -381,7 +381,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -439,7 +439,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -476,7 +476,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
isOpen={issueCrudState?.existing?.toggle}
|
||||
handleClose={() => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
toggleSubIssuesModal(false);
|
||||
toggleSubIssuesModal(null);
|
||||
}}
|
||||
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
|
||||
handleOnSubmit={(_issue) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
|
||||
// components
|
||||
import { TextArea } from "@plane/ui";
|
||||
// types
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { TIssueOperations } from "./issue-detail";
|
||||
// hooks
|
||||
@@ -16,12 +17,26 @@ export type IssueTitleInputProps = {
|
||||
issueOperations: TIssueOperations;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export const IssueTitleInput: FC<IssueTitleInputProps> = observer((props) => {
|
||||
const { disabled, value, workspaceSlug, isSubmitting, setIsSubmitting, issueId, issueOperations, projectId } = props;
|
||||
const {
|
||||
disabled,
|
||||
value,
|
||||
workspaceSlug,
|
||||
isSubmitting,
|
||||
setIsSubmitting,
|
||||
issueId,
|
||||
issueOperations,
|
||||
projectId,
|
||||
className,
|
||||
containerClassName,
|
||||
} = props;
|
||||
// states
|
||||
const [title, setTitle] = useState("");
|
||||
const [isLengthVisible, setIsLengthVisible] = useState(false);
|
||||
// hooks
|
||||
const debouncedValue = useDebounce(title, 1500);
|
||||
|
||||
@@ -76,19 +91,32 @@ export const IssueTitleInput: FC<IssueTitleInputProps> = observer((props) => {
|
||||
if (disabled) return <div className="text-2xl font-medium">{title}</div>;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={cn("relative", containerClassName)}>
|
||||
<TextArea
|
||||
id="title-input"
|
||||
className={`min-h-min block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-2xl font-medium outline-none ring-0 focus:ring-1 focus:ring-custom-primary ${
|
||||
title?.length === 0 ? "!ring-red-400" : ""
|
||||
}`}
|
||||
className={cn(
|
||||
"block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-0 text-2xl font-medium outline-none ring-0",
|
||||
{
|
||||
"ring-red-400": title.length === 0,
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={255}
|
||||
placeholder="Issue title"
|
||||
onFocus={() => setIsLengthVisible(true)}
|
||||
onBlur={() => setIsLengthVisible(false)}
|
||||
/>
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 p-0.5 text-xs text-custom-text-200 opacity-0 transition-opacity",
|
||||
{
|
||||
"opacity-100": isLengthVisible,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className={`${title.length === 0 || title.length > 255 ? "text-red-500" : ""}`}>{title.length}</span>
|
||||
/255
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,13 @@ import { useRouter } from "next/router";
|
||||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules";
|
||||
import {
|
||||
ModuleCardItem,
|
||||
ModuleListItem,
|
||||
ModulePeekOverview,
|
||||
ModuleViewHeader,
|
||||
ModulesListGanttChartView,
|
||||
} from "@/components/modules";
|
||||
import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
@@ -67,6 +73,12 @@ export const ModulesListView: React.FC = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-[50px] flex-shrink-0 w-full border-b border-custom-border-200 px-6 relative flex items-center gap-4 justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="block text-sm font-medium">Module name</span>
|
||||
</div>
|
||||
<ModuleViewHeader />
|
||||
</div>
|
||||
{displayFilters?.layout === "list" && (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="flex h-full w-full justify-between">
|
||||
|
||||
@@ -176,6 +176,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -104,6 +104,7 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
|
||||
item.action();
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className="h-3 w-3" />}
|
||||
{item.title}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { PageContentBrowser, PageEditorTitle } from "@/components/pages";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
@@ -68,7 +69,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
|
||||
const pageTitle = pageStore?.name ?? "";
|
||||
const pageDescription = pageStore?.description_html ?? "<p></p>";
|
||||
const isFullWidth = !!pageStore?.view_props?.full_width;
|
||||
const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore;
|
||||
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
|
||||
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
|
||||
@@ -79,6 +79,8 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
members: projectMemberDetails,
|
||||
user: currentUser ?? undefined,
|
||||
});
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
|
||||
|
||||
@@ -91,8 +93,8 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
<div
|
||||
className={cn("sticky top-0 hidden h-full flex-shrink-0 -translate-x-full p-5 duration-200 md:block", {
|
||||
"translate-x-0": sidePeekVisible,
|
||||
"w-56 lg:w-72": !isFullWidth,
|
||||
"w-[10%]": isFullWidth,
|
||||
"w-40 lg:w-56": !isFullWidth,
|
||||
"w-[5%]": isFullWidth,
|
||||
})}
|
||||
>
|
||||
{!isFullWidth && (
|
||||
@@ -104,8 +106,8 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div
|
||||
className={cn("h-full w-full pt-5", {
|
||||
"md:w-[calc(100%-14rem)] xl:w-[calc(100%-18rem-18rem)]": !isFullWidth,
|
||||
"md:w-[80%]": isFullWidth,
|
||||
"md:w-[calc(100%-10rem)] xl:w-[calc(100%-14rem-14rem)]": !isFullWidth,
|
||||
"md:w-[90%]": isFullWidth,
|
||||
})}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col gap-y-7 overflow-y-auto overflow-x-hidden">
|
||||
@@ -164,8 +166,8 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
<div
|
||||
className={cn("hidden xl:block flex-shrink-0", {
|
||||
"w-56 lg:w-72": !isFullWidth,
|
||||
"w-[10%]": isFullWidth,
|
||||
"w-40 lg:w-56": !isFullWidth,
|
||||
"w-[5%]": isFullWidth,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { observer } from "mobx-react";
|
||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/document-editor";
|
||||
// components
|
||||
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// store
|
||||
import { IPageStore } from "@/store/pages/page.store";
|
||||
|
||||
@@ -34,8 +36,9 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
setSidePeekVisible,
|
||||
} = props;
|
||||
// derived values
|
||||
const { isContentEditable, view_props } = pageStore;
|
||||
const isFullWidth = !!view_props?.full_width;
|
||||
const { isContentEditable } = pageStore;
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
|
||||
if (!editorRef.current && !readOnlyEditorRef.current) return null;
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ import { ArchiveRestoreIcon, Clipboard, Copy, Link, Lock, LockOpen } from "lucid
|
||||
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/document-editor";
|
||||
// ui
|
||||
import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useApplication, useUser } from "@/hooks/store";
|
||||
import { useApplication } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// store
|
||||
import { IPageStore } from "@/store/pages/page.store";
|
||||
|
||||
@@ -34,18 +32,13 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
canCurrentUserDuplicatePage,
|
||||
canCurrentUserLockPage,
|
||||
restore,
|
||||
view_props,
|
||||
updateViewProps,
|
||||
} = pageStore;
|
||||
// store hooks
|
||||
const {
|
||||
router: { workspaceSlug, projectId },
|
||||
} = useApplication();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
// auth
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
// page filters
|
||||
const { isFullWidth, handleFullWidth } = usePageFilters();
|
||||
|
||||
const handleArchivePage = async () =>
|
||||
await archive().catch(() =>
|
||||
@@ -149,22 +142,10 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
<CustomMenu maxHeight="md" placement="bottom-start" verticalEllipsis closeOnSelect>
|
||||
<CustomMenu.MenuItem
|
||||
className="hidden md:flex w-full items-center justify-between gap-2"
|
||||
onClick={() =>
|
||||
updateViewProps({
|
||||
full_width: !view_props?.full_width,
|
||||
})
|
||||
}
|
||||
disabled={!isEditingAllowed}
|
||||
onClick={() => handleFullWidth(!isFullWidth)}
|
||||
>
|
||||
Full width
|
||||
<ToggleSwitch
|
||||
value={!!view_props?.full_width}
|
||||
onChange={() => {}}
|
||||
className={cn({
|
||||
"opacity-40": !isEditingAllowed,
|
||||
})}
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
<ToggleSwitch value={isFullWidth} onChange={() => {}} />
|
||||
</CustomMenu.MenuItem>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (!item.shouldRender) return null;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/document-ed
|
||||
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// store
|
||||
import { IPageStore } from "@/store/pages/page.store";
|
||||
|
||||
@@ -36,8 +38,9 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
setSidePeekVisible,
|
||||
} = props;
|
||||
// derived values
|
||||
const { isContentEditable, view_props } = pageStore;
|
||||
const isFullWidth = !!view_props?.full_width;
|
||||
const { isContentEditable } = pageStore;
|
||||
// page filters
|
||||
const { isFullWidth } = usePageFilters();
|
||||
|
||||
if (!editorRef.current && !readOnlyEditorRef.current) return null;
|
||||
|
||||
@@ -46,8 +49,8 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
|
||||
<div className="hidden md:flex items-center border-b border-custom-border-200 px-3 py-2 md:px-5">
|
||||
<div
|
||||
className={cn("flex-shrink-0", {
|
||||
"w-56 lg:w-72": !isFullWidth,
|
||||
"w-[10%]": isFullWidth,
|
||||
"w-40 lg:w-56": !isFullWidth,
|
||||
"w-[5%]": isFullWidth,
|
||||
})}
|
||||
>
|
||||
<PageSummaryPopover
|
||||
|
||||
@@ -23,7 +23,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
{readOnly ? (
|
||||
<h6
|
||||
className="break-words bg-transparent text-4xl font-bold"
|
||||
className="break-words bg-transparent text-[1.75rem] font-semibold"
|
||||
style={{
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
@@ -34,7 +34,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
||||
<>
|
||||
<TextArea
|
||||
onChange={(e) => updateTitle(e.target.value)}
|
||||
className="w-full bg-custom-background text-4xl font-bold outline-none p-0 border-none resize-none rounded-none"
|
||||
className="w-full bg-custom-background text-[1.75rem] font-semibold outline-none p-0 border-none resize-none rounded-none"
|
||||
style={{
|
||||
lineHeight: "1.2",
|
||||
}}
|
||||
|
||||
@@ -123,14 +123,14 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
||||
action: handleOpenInNewTab,
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
shouldRender: project.is_member,
|
||||
shouldRender: project.is_member && !isArchived,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
action: handleCopyText,
|
||||
title: "Copy link",
|
||||
icon: LinkIcon,
|
||||
shouldRender: true,
|
||||
shouldRender: !isArchived,
|
||||
},
|
||||
{
|
||||
key: "restore",
|
||||
|
||||
@@ -103,6 +103,7 @@ export const ViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, LinkIcon } from "lucide-react";
|
||||
// ui
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
import { ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
workspaceSlug: string;
|
||||
globalViewId: string | undefined;
|
||||
view: {
|
||||
key: TStaticViewTypes;
|
||||
label: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const DefaultWorkspaceViewQuickActions: React.FC<Props> = observer((props) => {
|
||||
const { parentRef, globalViewId, view, workspaceSlug } = props;
|
||||
|
||||
const viewLink = `${workspaceSlug}/workspace-views/${view.key}`;
|
||||
const handleCopyText = () =>
|
||||
copyUrlToClipboard(viewLink).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "View link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
const handleOpenInNewTab = () => window.open(`/${viewLink}`, "_blank");
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "open-new-tab",
|
||||
action: handleOpenInNewTab,
|
||||
title: "Open in new tab",
|
||||
icon: ExternalLink,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
action: handleCopyText,
|
||||
title: "Copy link",
|
||||
icon: LinkIcon,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<>
|
||||
{view.key === globalViewId ? (
|
||||
<span
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
||||
view.key === globalViewId
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
}`}
|
||||
>
|
||||
{view.label}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
key={view.key}
|
||||
id={`global-view-${view.key}`}
|
||||
href={`/${workspaceSlug}/workspace-views/${view.key}`}
|
||||
>
|
||||
<span
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
||||
view.key === globalViewId
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
}`}
|
||||
>
|
||||
{view.label}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
placement="bottom-end"
|
||||
menuItemsClassName="z-20"
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
{
|
||||
"text-custom-text-400": item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { Plus } from "lucide-react";
|
||||
// types
|
||||
import { TStaticViewTypes } from "@plane/types";
|
||||
// components
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
|
||||
import {
|
||||
CreateUpdateWorkspaceViewModal,
|
||||
DefaultWorkspaceViewQuickActions,
|
||||
WorkspaceViewQuickActions,
|
||||
} from "@/components/workspace";
|
||||
// constants
|
||||
import { GLOBAL_VIEW_OPENED } from "@/constants/event-tracker";
|
||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "@/constants/workspace";
|
||||
@@ -14,6 +19,8 @@ import { useEventTracker, useGlobalView, useUser } from "@/hooks/store";
|
||||
|
||||
const ViewTab = observer((props: { viewId: string }) => {
|
||||
const { viewId } = props;
|
||||
// refs
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId } = router.query;
|
||||
@@ -22,30 +29,54 @@ const ViewTab = observer((props: { viewId: string }) => {
|
||||
|
||||
const view = getViewDetailsById(viewId);
|
||||
|
||||
if (!view) return null;
|
||||
if (!view || !workspaceSlug || !globalViewId) return null;
|
||||
|
||||
return (
|
||||
<Link key={viewId} id={`global-view-${viewId}`} href={`/${workspaceSlug}/workspace-views/${viewId}`}>
|
||||
<span
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
||||
viewId === globalViewId
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
}`}
|
||||
>
|
||||
{view.name}
|
||||
</span>
|
||||
</Link>
|
||||
<div ref={parentRef} className="relative">
|
||||
<WorkspaceViewQuickActions
|
||||
parentRef={parentRef}
|
||||
view={view}
|
||||
viewId={viewId}
|
||||
globalViewId={globalViewId?.toString()}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DefaultViewTab = (props: {
|
||||
tab: {
|
||||
key: TStaticViewTypes;
|
||||
label: string;
|
||||
};
|
||||
}) => {
|
||||
const { tab } = props;
|
||||
// refs
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId } = router.query;
|
||||
|
||||
if (!workspaceSlug || !globalViewId) return null;
|
||||
return (
|
||||
<div key={tab.key} ref={parentRef} className="relative">
|
||||
<DefaultWorkspaceViewQuickActions
|
||||
parentRef={parentRef}
|
||||
globalViewId={globalViewId?.toString()}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
view={tab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GlobalViewsHeader: React.FC = observer(() => {
|
||||
// states
|
||||
const [createViewModal, setCreateViewModal] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId } = router.query;
|
||||
const { globalViewId } = router.query;
|
||||
// store hooks
|
||||
const { currentWorkspaceViews } = useGlobalView();
|
||||
const {
|
||||
@@ -82,23 +113,11 @@ export const GlobalViewsHeader: React.FC = observer(() => {
|
||||
ref={containerRef}
|
||||
className="flex w-full items-center overflow-x-auto px-4 horizontal-scrollbar scrollbar-sm"
|
||||
>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => (
|
||||
<Link key={tab.key} id={`global-view-${tab.key}`} href={`/${workspaceSlug}/workspace-views/${tab.key}`}>
|
||||
<span
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 p-3 text-sm font-medium outline-none ${
|
||||
tab.key === globalViewId
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent hover:border-custom-border-200 hover:text-custom-text-400"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
</Link>
|
||||
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab, index) => (
|
||||
<DefaultViewTab key={`${tab.key}-${index}`} tab={tab} />
|
||||
))}
|
||||
|
||||
{currentWorkspaceViews?.map((viewId) => (
|
||||
<ViewTab key={viewId} viewId={viewId} />
|
||||
))}
|
||||
{currentWorkspaceViews?.map((viewId) => <ViewTab key={viewId} viewId={viewId} />)}
|
||||
</div>
|
||||
|
||||
{isAuthorizedUser && (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user