mirror of
https://github.com/makeplane/plane.git
synced 2026-07-14 14:31:37 +02:00
Merge branch 'preview' of github.com:makeplane/plane-ee into develop
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
@@ -7,23 +7,23 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2"
|
||||
RUN apk add --no-cache \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec"
|
||||
|
||||
COPY requirements.txt ./
|
||||
COPY requirements ./requirements
|
||||
RUN apk add --no-cache libffi-dev
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
"bash~=5.2" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
@@ -7,18 +7,18 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2" \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec" \
|
||||
"libffi-dev" \
|
||||
"bash~=5.2" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
"xmlsec-dev"
|
||||
|
||||
@@ -47,6 +47,12 @@ class IssueSerializer(BaseSerializer):
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
type_id = serializers.PrimaryKeyRelatedField(
|
||||
source="type",
|
||||
queryset=IssueType.objects.all(),
|
||||
required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Issue
|
||||
@@ -130,19 +136,14 @@ class IssueSerializer(BaseSerializer):
|
||||
workspace_id = self.context["workspace_id"]
|
||||
default_assignee_id = self.context["default_assignee_id"]
|
||||
|
||||
issue_type_id = validated_data.get("type_id")
|
||||
issue_type = validated_data.pop("type", None)
|
||||
|
||||
if issue_type_id:
|
||||
# Check if issue type is valid
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_id=project_id, id=issue_type_id, is_active=True
|
||||
).first()
|
||||
else:
|
||||
if not issue_type:
|
||||
# Get default issue type
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_id=project_id,
|
||||
is_default=True,
|
||||
project_issue_types__project_id=project_id, is_default=True
|
||||
).first()
|
||||
issue_type = issue_type
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**validated_data,
|
||||
|
||||
@@ -53,6 +53,7 @@ class IssueFlatSerializer(BaseSerializer):
|
||||
"sequence_id",
|
||||
"sort_order",
|
||||
"is_draft",
|
||||
"type_id",
|
||||
]
|
||||
|
||||
|
||||
@@ -147,7 +148,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
if not issue_type:
|
||||
# Get default issue type
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_id=project_id, is_default=True
|
||||
project_issue_types__project_id=project_id, is_default=True
|
||||
).first()
|
||||
issue_type = issue_type
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"type_id",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -143,6 +143,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"relation_type",
|
||||
"type_id",
|
||||
]
|
||||
|
||||
response_data = {
|
||||
|
||||
@@ -133,6 +133,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
"link_count",
|
||||
"is_draft",
|
||||
"archived_at",
|
||||
"type_id",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
sub_issues = user_timezone_converter(
|
||||
|
||||
@@ -91,6 +91,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
"project__identifier",
|
||||
"project_id",
|
||||
"workspace__slug",
|
||||
"type_id",
|
||||
)
|
||||
|
||||
def filter_cycles(self, query, slug, project_id, workspace_search):
|
||||
@@ -368,6 +369,7 @@ class SearchEndpoint(BaseAPIView):
|
||||
"project_id",
|
||||
"priority",
|
||||
"state_id",
|
||||
"type_id",
|
||||
)[:count]
|
||||
)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -95,6 +95,7 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"state__name",
|
||||
"state__group",
|
||||
"state__color",
|
||||
"type_id",
|
||||
),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -119,6 +119,7 @@ class WorkspaceEntitySearchEndpoint(BaseAPIView):
|
||||
"project_id",
|
||||
"priority",
|
||||
"state_id",
|
||||
"type_id",
|
||||
)[:count]
|
||||
)
|
||||
|
||||
|
||||
@@ -109,6 +109,6 @@ from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
|
||||
from .favorite import UserFavorite
|
||||
|
||||
from .issue_type import IssueType
|
||||
from .issue_type import IssueType, ProjectIssueType
|
||||
|
||||
from .recent_visit import UserRecentVisit
|
||||
from .recent_visit import UserRecentVisit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.2.14 on 2024-07-31 14:22
|
||||
# Generated by Django 4.2.14 on 2024-08-02 09:52
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
@@ -13,43 +13,8 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="issueproperty",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyactivity",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyoption",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyvalue",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueworklog",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="WorkspaceFeature",
|
||||
name="ProjectAttribute",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
@@ -81,43 +46,27 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_project_grouping_enabled",
|
||||
models.BooleanField(default=False),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="features",
|
||||
to="db.workspace",
|
||||
"priority",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("none", "None"),
|
||||
("low", "Low"),
|
||||
("medium", "Medium"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
],
|
||||
default="none",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("start_date", models.DateTimeField(blank=True, null=True)),
|
||||
("target_date", models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Workspace Feature",
|
||||
"verbose_name_plural": "Workspace Features",
|
||||
"db_table": "workspace_features",
|
||||
"ordering": ("-created_at",),
|
||||
"verbose_name": "Project Attribute",
|
||||
"verbose_name_plural": "Project Attributes",
|
||||
"db_table": "project_attributes",
|
||||
"ordering": ("created_at",),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -180,45 +129,16 @@ class Migration(migrations.Migration):
|
||||
"external_id",
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_states",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Project State",
|
||||
"verbose_name_plural": "Project States",
|
||||
"db_table": "project_states",
|
||||
"ordering": ("sequence",),
|
||||
"unique_together": {("name", "workspace")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ProjectAttribute",
|
||||
name="WorkspaceFeature",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
@@ -250,72 +170,206 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
(
|
||||
"priority",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("none", "None"),
|
||||
("low", "Low"),
|
||||
("medium", "Medium"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
],
|
||||
default="none",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("start_date", models.DateTimeField(blank=True, null=True)),
|
||||
("target_date", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"state",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="project_attributes",
|
||||
to="ee.projectstate",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
"is_project_grouping_enabled",
|
||||
models.BooleanField(default=False),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Project Attribute",
|
||||
"verbose_name_plural": "Project Attributes",
|
||||
"db_table": "project_attributes",
|
||||
"ordering": ("created_at",),
|
||||
"verbose_name": "Workspace Feature",
|
||||
"verbose_name_plural": "Workspace Features",
|
||||
"db_table": "workspace_features",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="issueproperty",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="issuepropertyoption",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueproperty",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyactivity",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyoption",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issuepropertyvalue",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueworklog",
|
||||
name="deleted_at",
|
||||
field=models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="issueproperty",
|
||||
unique_together={("name", "issue_type", "deleted_at")},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="issuepropertyoption",
|
||||
unique_together={("name", "property", "deleted_at")},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="issueproperty",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("name", "issue_type"),
|
||||
name="issue_property_unique_name_project_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="issuepropertyoption",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("name", "property"),
|
||||
name="issue_property_option_unique_name_project_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspacefeature",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspacefeature",
|
||||
name="updated_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspacefeature",
|
||||
name="workspace",
|
||||
field=models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="features",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectstate",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectstate",
|
||||
name="updated_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectstate",
|
||||
name="workspace",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_states",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectattribute",
|
||||
name="created_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectattribute",
|
||||
name="project",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectattribute",
|
||||
name="state",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="project_attributes",
|
||||
to="ee.projectstate",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectattribute",
|
||||
name="updated_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="projectattribute",
|
||||
name="workspace",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="projectstate",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("deleted_at__isnull", True)),
|
||||
fields=("name", "workspace"),
|
||||
name="project_state_unique_name_project_when_deleted_at_null",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="projectstate",
|
||||
unique_together={("name", "workspace", "deleted_at")},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.14 on 2024-08-14 10:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ee", "0004_workspacelicense_trial_end_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workspacelicense",
|
||||
name="has_activated_free_trial",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspacelicense",
|
||||
name="has_added_payment_method",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,10 @@ class WorkspaceLicense(BaseModel):
|
||||
last_synced_at = models.DateTimeField(default=timezone.now)
|
||||
# trial end date
|
||||
trial_end_date = models.DateTimeField(null=True, blank=True)
|
||||
# has activated free trial
|
||||
has_activated_free_trial = models.BooleanField(default=False)
|
||||
# is payment method added
|
||||
has_added_payment_method = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Workspace License"
|
||||
|
||||
@@ -13,6 +13,10 @@ from plane.ee.models import (
|
||||
|
||||
class IssueTypeSerializer(BaseSerializer):
|
||||
issue_exists = serializers.BooleanField(read_only=True)
|
||||
project_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IssueType
|
||||
@@ -34,7 +38,6 @@ class IssuePropertySerializer(BaseSerializer):
|
||||
"name",
|
||||
"issue_type",
|
||||
"workspace",
|
||||
"project",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
@@ -47,7 +50,6 @@ class IssuePropertyOptionSerializer(BaseSerializer):
|
||||
read_only_fields = [
|
||||
"property",
|
||||
"workspace",
|
||||
"project",
|
||||
"deleted_at",
|
||||
]
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ class IssuePropertyActivityEndpoint(BaseAPIView):
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_DISPLAY)
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
# Get the filters
|
||||
filters = {}
|
||||
if request.GET.get("created_at__gt", None) is not None:
|
||||
filters = {"created_at__gt": request.GET.get("created_at__gt")}
|
||||
|
||||
# Get the order by
|
||||
order_by = request.GET.get("order_by", "-created_at")
|
||||
|
||||
@@ -27,6 +32,7 @@ class IssuePropertyActivityEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_id=issue_id,
|
||||
**filters
|
||||
).order_by(order_by)
|
||||
|
||||
# Serialize the data
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Django imports
|
||||
from django.db.models import Exists, OuterRef
|
||||
from django.db.models import Exists, OuterRef, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -7,7 +9,7 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from plane.ee.views.base import BaseAPIView
|
||||
from plane.db.models import IssueType, Issue, Project
|
||||
from plane.db.models import IssueType, Issue, Project, ProjectIssueType
|
||||
from plane.ee.permissions import (
|
||||
ProjectEntityPermission,
|
||||
WorkspaceEntityPermission,
|
||||
@@ -25,17 +27,35 @@ class WorkspaceIssueTypeEndpoint(BaseAPIView):
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_DISPLAY)
|
||||
def get(self, request, slug):
|
||||
# Get all issue types for the workspace
|
||||
issue_types = IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
).annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, type_id=OuterRef("pk")
|
||||
issue_types = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project__project_projectmember__member=request.user,
|
||||
project_issue_types__project__project_projectmember__is_active=True,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
workspace__slug=slug, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).order_by("created_at")
|
||||
serializer = IssueTypeSerializer(issue_types, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -54,44 +74,111 @@ class IssueTypeEndpoint(BaseAPIView):
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(project_id=project_id, type_id=pk)
|
||||
)
|
||||
).get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
).get(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
serializer = IssueTypeSerializer(issue_type)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# Get all issue types
|
||||
issue_types = IssueType.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
issue_types = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).order_by("created_at")
|
||||
|
||||
serializer = IssueTypeSerializer(issue_types, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
# @check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def post(self, request, slug, project_id):
|
||||
# Fetch the project
|
||||
project = Project.objects.get(pk=project_id)
|
||||
# Create a new issue type
|
||||
serializer = IssueTypeSerializer(data=request.data)
|
||||
# check weight
|
||||
if not request.data.get("weight"):
|
||||
request.data["weight"] = 1
|
||||
# Check is_active
|
||||
if not request.data.get("is_active"):
|
||||
request.data["is_active"] = False
|
||||
# Validate the data
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# Save the data
|
||||
serializer.save(project_id=project_id)
|
||||
serializer.save(workspace_id=project.workspace_id)
|
||||
|
||||
# Bridge the issue type with the project
|
||||
ProjectIssueType.objects.create(
|
||||
project_id=project_id,
|
||||
issue_type_id=serializer.data["id"],
|
||||
level=0,
|
||||
)
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
# Serialize the data
|
||||
serializer = IssueTypeSerializer(issue_type)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
# Update an issue type
|
||||
issue_type = IssueType.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
# Default cannot be made in active
|
||||
@@ -110,13 +197,50 @@ class IssueTypeEndpoint(BaseAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# Save the data
|
||||
serializer.save()
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=serializer.data["id"],
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Serialize the data
|
||||
serializer = IssueTypeSerializer(issue_type.first())
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@check_feature_flag(FeatureFlag.ISSUE_TYPE_SETTINGS)
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
# Delete an issue type
|
||||
issue_type = IssueType.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
# Check if there are any issues using this issue type
|
||||
@@ -150,32 +274,39 @@ class DefaultIssueTypeEndpoint(BaseAPIView):
|
||||
|
||||
# If issue type is already created return an error
|
||||
if IssueType.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
is_default=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{{"error": "Default issue type already exists"}},
|
||||
{"error": "Default issue type already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if default issue type exists for the project
|
||||
if ProjectIssueType.objects.filter(
|
||||
project_id=project_id, is_default=True
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "Default issue type already exists"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Create a new default issue type
|
||||
issue_type, _ = IssueType.objects.get_or_create(
|
||||
project_id=project_id,
|
||||
issue_type = IssueType.objects.create(
|
||||
workspace_id=project.workspace_id,
|
||||
name="Issue",
|
||||
is_default=True,
|
||||
defaults={
|
||||
"description": "Default issue type with the option to add new properties",
|
||||
"is_default": True,
|
||||
"weight": 0,
|
||||
"sort_order": 1,
|
||||
"logo_props": {
|
||||
"in_use": "icon",
|
||||
"icon": {
|
||||
"color": "#ffffff",
|
||||
"background_color": "#6695FF",
|
||||
},
|
||||
description="Default issue type with the option to add new properties",
|
||||
logo_props={
|
||||
"in_use": "icon",
|
||||
"icon": {
|
||||
"color": "#ffffff",
|
||||
"background_color": "#6695FF",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# Update existing issues to use the new default issue type
|
||||
Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
@@ -187,5 +318,46 @@ class DefaultIssueTypeEndpoint(BaseAPIView):
|
||||
project.is_issue_type_enabled = True
|
||||
project.save()
|
||||
|
||||
# Bridge the issue type with the project
|
||||
ProjectIssueType.objects.create(
|
||||
project_id=project_id,
|
||||
issue_type_id=issue_type.id,
|
||||
level=0,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Refetch the data
|
||||
issue_type = (
|
||||
IssueType.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_issue_types__project_id=project_id,
|
||||
pk=issue_type.id,
|
||||
)
|
||||
.annotate(
|
||||
issue_exists=Exists(
|
||||
Issue.objects.filter(
|
||||
project_id=project_id, type_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
Subquery(
|
||||
ProjectIssueType.objects.filter(
|
||||
issue_type=OuterRef("pk"), workspace__slug=slug
|
||||
)
|
||||
.values("issue_type")
|
||||
.annotate(
|
||||
project_ids=ArrayAgg("project_id", distinct=True)
|
||||
)
|
||||
.values("project_ids")
|
||||
),
|
||||
[],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Serialize the data
|
||||
return Response(str(issue_type.id), status=status.HTTP_201_CREATED)
|
||||
serializer = IssueTypeSerializer(issue_type.first())
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -230,9 +230,16 @@ class IssuePropertyValueEndpoint(BaseAPIView):
|
||||
existing_prop_queryset
|
||||
).values("property_id", "values")
|
||||
|
||||
# existing values
|
||||
existing_values = {
|
||||
str(prop["property_id"]): prop["values"]
|
||||
for prop in existing_prop_values
|
||||
}
|
||||
|
||||
# Get the value
|
||||
values = request.data.get("values", [])
|
||||
|
||||
# Check if the property is required
|
||||
if issue_property.is_required and (
|
||||
not values or not [v for v in values if v]
|
||||
):
|
||||
@@ -277,10 +284,7 @@ class IssuePropertyValueEndpoint(BaseAPIView):
|
||||
|
||||
# Log the activity
|
||||
issue_property_activity.delay(
|
||||
existing_values={
|
||||
str(prop["property_id"]): prop["values"]
|
||||
for prop in existing_prop_values
|
||||
},
|
||||
existing_values=existing_values,
|
||||
requested_values={str(property_id): values},
|
||||
issue_id=issue_id,
|
||||
user_id=str(request.user.id),
|
||||
|
||||
@@ -102,6 +102,12 @@ def member_sync_task(slug):
|
||||
workspace_license.trial_end_date = updated_workspace_license[
|
||||
"trial_end_date"
|
||||
]
|
||||
workspace_license.has_activated_free_trial = (
|
||||
updated_workspace_license["has_activated_free_trial"]
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
updated_workspace_license["has_added_payment_method"]
|
||||
)
|
||||
workspace_license.last_synced_at = timezone.now()
|
||||
workspace_license.save()
|
||||
|
||||
@@ -120,6 +126,12 @@ def member_sync_task(slug):
|
||||
"is_offline_payment"
|
||||
],
|
||||
last_synced_at=timezone.now(),
|
||||
has_activated_free_trial=updated_workspace_license[
|
||||
"has_activated_free_trial"
|
||||
],
|
||||
has_added_payment_method=updated_workspace_license[
|
||||
"has_added_payment_method"
|
||||
],
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ from .views import (
|
||||
WorkspaceLicenseRefreshEndpoint,
|
||||
WorkspaceLicenseSyncEndpoint,
|
||||
WorkspaceFreeTrialEndpoint,
|
||||
WorkspaceTrialUpgradeEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -68,6 +69,11 @@ urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/trial-subscriptions/",
|
||||
WorkspaceFreeTrialEndpoint.as_view(),
|
||||
name="license-refresh",
|
||||
name="trial-subscriptions",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/trial-subscriptions/upgrade/",
|
||||
WorkspaceTrialUpgradeEndpoint.as_view(),
|
||||
name="trial-upgrade",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ from .payment import (
|
||||
PaymentLinkEndpoint,
|
||||
WebsitePaymentLinkEndpoint,
|
||||
WorkspaceFreeTrialEndpoint,
|
||||
WorkspaceTrialUpgradeEndpoint,
|
||||
)
|
||||
from .subscription import SubscriptionEndpoint, UpgradeSubscriptionEndpoint
|
||||
from .feature_flag import FeatureFlagProxyEndpoint
|
||||
|
||||
@@ -246,7 +246,7 @@ class WorkspaceFreeTrialEndpoint(BaseAPIView):
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error fetching product details"},
|
||||
{"error": "error upgrading trial subscriptions"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
@@ -257,6 +257,87 @@ class WorkspaceFreeTrialEndpoint(BaseAPIView):
|
||||
)
|
||||
log_exception(e)
|
||||
return Response(
|
||||
{"error": "error fetching payment link"},
|
||||
{"error": "error creating trial subscriptions"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceTrialUpgradeEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
# Get the product_id and price_id
|
||||
price_id = request.data.get("price_id", False)
|
||||
|
||||
# Check if product_id and price_id are present
|
||||
if not price_id:
|
||||
return Response(
|
||||
{"error": "price_id is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the workspace members
|
||||
workspace_members = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True, member__is_bot=False
|
||||
)
|
||||
.annotate(
|
||||
user_email=F("member__email"),
|
||||
user_id=F("member__id"),
|
||||
user_role=F("role"),
|
||||
)
|
||||
.values(
|
||||
"user_email",
|
||||
"user_id",
|
||||
"user_role",
|
||||
)
|
||||
.values("user_email", "user_id", "user_role")
|
||||
)
|
||||
|
||||
# Convert the user_id to string
|
||||
for member in workspace_members:
|
||||
member["user_id"] = str(member["user_id"])
|
||||
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# Check if the payment server base url is present
|
||||
if settings.PAYMENT_SERVER_BASE_URL:
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/trial-subscriptions/upgrade/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={
|
||||
"workspace_id": str(workspace.id),
|
||||
"stripe_price_id": price_id,
|
||||
"members_list": list(workspace_members),
|
||||
"slug": slug,
|
||||
},
|
||||
)
|
||||
# Check if the response is successful
|
||||
response.raise_for_status()
|
||||
# Convert the response to json
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error upgrading trial subscriptions"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
if e.response.status_code == 400:
|
||||
return Response(
|
||||
e.response.json(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
log_exception(e)
|
||||
return Response(
|
||||
{"error": "error upgrading trial subscriptions"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
||||
from django.db.models import CharField
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils import timezone
|
||||
from django.db.models import F
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -94,48 +95,36 @@ class WebsiteUserWorkspaceEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
# Get all the workspaces where the user is admin
|
||||
workspace_query = (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role=20,
|
||||
)
|
||||
.annotate(uuid_str=Cast("workspace_id", CharField()))
|
||||
workspace_ids = WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role=20,
|
||||
).values_list("workspace_id", flat=True)
|
||||
|
||||
# Fetch the workspaces from the workspace license
|
||||
workspace_licenses = (
|
||||
WorkspaceLicense.objects.filter(workspace_id__in=workspace_ids)
|
||||
.annotate(slug=F("workspace__slug"))
|
||||
.annotate(name=F("workspace__name"))
|
||||
.annotate(logo=F("workspace__logo"))
|
||||
.annotate(product=F("plan"))
|
||||
.values(
|
||||
"uuid_str",
|
||||
"workspace__slug",
|
||||
"workspace__name",
|
||||
"workspace__logo",
|
||||
"workspace_id",
|
||||
"slug",
|
||||
"name",
|
||||
"logo",
|
||||
"product",
|
||||
"trial_end_date",
|
||||
"has_activated_free_trial",
|
||||
"has_added_payment_method",
|
||||
"current_period_end_date",
|
||||
"is_offline_payment",
|
||||
)
|
||||
)
|
||||
|
||||
workspaces = [
|
||||
{
|
||||
"workspace_id": workspace["uuid_str"],
|
||||
"slug": workspace["workspace__slug"],
|
||||
"name": workspace["workspace__name"],
|
||||
"logo": workspace["workspace__logo"],
|
||||
}
|
||||
for workspace in workspace_query
|
||||
]
|
||||
# Get the workspace details
|
||||
return Response(workspace_licenses, status=status.HTTP_200_OK)
|
||||
|
||||
if settings.PAYMENT_SERVER_BASE_URL:
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/user-workspace-products/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={"workspaces": workspaces},
|
||||
)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error fetching product details"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
if e.response.status_code == 400:
|
||||
return Response(
|
||||
@@ -204,6 +193,15 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
workspace_license.is_offline_payment = response.get(
|
||||
"is_offline_payment", False
|
||||
)
|
||||
workspace_license.trial_end_date = response.get(
|
||||
"trial_end_date"
|
||||
)
|
||||
workspace_license.has_activated_free_trial = (
|
||||
response.get("has_activated_free_trial", False)
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
response.get("has_added_payment_method", False)
|
||||
)
|
||||
workspace_license.save()
|
||||
|
||||
return Response(
|
||||
@@ -215,6 +213,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -228,6 +228,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -256,6 +258,12 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
plan=response.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=response.get("trial_end_date"),
|
||||
has_activated_free_trial=response.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=response.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
# Return the workspace license
|
||||
return Response(
|
||||
@@ -267,6 +275,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -284,7 +294,6 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
@@ -318,6 +327,12 @@ class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
workspace_license.recurring_interval = response.get("interval")
|
||||
workspace_license.plan = response.get("plan")
|
||||
workspace_license.trial_end_date = response.get("trial_end_date")
|
||||
workspace_license.has_activated_free_trial = response.get(
|
||||
"has_activated_free_trial", False
|
||||
)
|
||||
workspace_license.has_added_payment_method = response.get(
|
||||
"has_added_payment_method", False
|
||||
)
|
||||
workspace_license.last_synced_at = timezone.now()
|
||||
workspace_license.save()
|
||||
# If the license is not present, then fetch the license from the payment server and create it
|
||||
@@ -345,6 +360,12 @@ class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
plan=response.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=response.get("trial_end_date"),
|
||||
has_activated_free_trial=response.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=response.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
@@ -352,7 +373,6 @@ class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class WorkspaceLicenseSyncEndpoint(BaseAPIView):
|
||||
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
]
|
||||
@@ -401,6 +421,12 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView):
|
||||
workspace_license.trial_end_date = request.data.get(
|
||||
"trial_end_date"
|
||||
)
|
||||
workspace_license.has_activated_free_trial = request.data.get(
|
||||
"has_activated_free_trial", False
|
||||
)
|
||||
workspace_license.has_added_payment_method = request.data.get(
|
||||
"has_added_payment_method", False
|
||||
)
|
||||
workspace_license.save()
|
||||
# If the workspace license is not present, then fetch the license from the payment server and create it
|
||||
else:
|
||||
@@ -417,6 +443,12 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView):
|
||||
plan=request.data.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=request.data.get("trial_end_date"),
|
||||
has_activated_free_trial=request.data.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=request.data.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
|
||||
@@ -143,6 +143,16 @@ class UpgradeSubscriptionEndpoint(BaseAPIView):
|
||||
workspace_license.trial_end_date = (
|
||||
workspace_license_response.get("trial_end_date")
|
||||
)
|
||||
workspace_license.has_activated_free_trial = (
|
||||
workspace_license_response.get(
|
||||
"has_activated_free_trial"
|
||||
)
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
workspace_license_response.get(
|
||||
"has_added_payment_method"
|
||||
)
|
||||
)
|
||||
workspace_license.save()
|
||||
else:
|
||||
# Create a new workspace license
|
||||
@@ -168,6 +178,12 @@ class UpgradeSubscriptionEndpoint(BaseAPIView):
|
||||
"trial_end_date"
|
||||
),
|
||||
plan=workspace_license_response.get("plan"),
|
||||
has_activated_free_trial=workspace_license_response.get(
|
||||
"has_activated_free_trial"
|
||||
),
|
||||
has_added_payment_method=workspace_license_response.get(
|
||||
"has_added_payment_method"
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
|
||||
@@ -524,6 +524,22 @@ def filter_logged_by(params, issue_filter, method, prefix=""):
|
||||
return issue_filter
|
||||
|
||||
|
||||
def filter_issue_type(params, issue_filter, method, prefix=""):
|
||||
if method == "GET":
|
||||
types = [item for item in params.get("issue_type").split(",") if item != "null"]
|
||||
types = filter_valid_uuids(types)
|
||||
if len(types) and "" not in types:
|
||||
issue_filter[f"{prefix}type__in"] = types
|
||||
else:
|
||||
if (
|
||||
params.get("issue_type", None)
|
||||
and len(params.get("issue_type"))
|
||||
and params.get("issue_type") != "null"
|
||||
):
|
||||
issue_filter[f"{prefix}type__in"] = params.get("issue_type")
|
||||
return issue_filter
|
||||
|
||||
|
||||
def issue_filters(query_params, method, prefix=""):
|
||||
issue_filter = {}
|
||||
|
||||
@@ -552,6 +568,7 @@ def issue_filters(query_params, method, prefix=""):
|
||||
"sub_issue": filter_sub_issue_toggle,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
"issue_type": filter_issue_type,
|
||||
}
|
||||
|
||||
for key, value in ISSUE_FILTER.items():
|
||||
|
||||
@@ -138,7 +138,7 @@ services:
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.5-alpine
|
||||
image: postgres:15.7-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: postgres -c 'max_connections=1000'
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
|
||||
plane-db:
|
||||
image: postgres:15.2-alpine
|
||||
image: postgres:15.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
|
||||
plane-db:
|
||||
container_name: plane-db
|
||||
image: postgres:15.2-alpine
|
||||
image: postgres:15.7-alpine
|
||||
restart: always
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
|
||||
1
packages/types/src/issues.d.ts
vendored
1
packages/types/src/issues.d.ts
vendored
@@ -140,6 +140,7 @@ export interface IIssueActivity {
|
||||
name: string;
|
||||
priority: string | null;
|
||||
sequence_id: string;
|
||||
type_id: string;
|
||||
} | null;
|
||||
new_identifier: string | null;
|
||||
new_value: string | null;
|
||||
|
||||
5
packages/types/src/issues/activity/base.d.ts
vendored
5
packages/types/src/issues/activity/base.d.ts
vendored
@@ -60,4 +60,9 @@ export type TIssueActivityComment =
|
||||
id: string;
|
||||
activity_type: "WORKLOG";
|
||||
created_at?: string;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
activity_type: "ISSUE_ADDITIONAL_PROPERTIES_ACTIVITY";
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
4
packages/types/src/payment.d.ts
vendored
4
packages/types/src/payment.d.ts
vendored
@@ -23,4 +23,8 @@ export type IWorkspaceProductSubscription = {
|
||||
interval?: "month" | "year" | null;
|
||||
current_period_end_date: string | null;
|
||||
is_offline_payment: boolean;
|
||||
trial_end_date: string | undefined;
|
||||
purchased_seats: number | undefined;
|
||||
has_activated_free_trial: boolean;
|
||||
has_added_payment_method: boolean;
|
||||
};
|
||||
|
||||
1
packages/types/src/project/projects.d.ts
vendored
1
packages/types/src/project/projects.d.ts
vendored
@@ -144,4 +144,5 @@ export interface ISearchIssueResponse {
|
||||
state__group: TStateGroups;
|
||||
state__name: string;
|
||||
workspace__slug: string;
|
||||
type_id: string;
|
||||
}
|
||||
|
||||
1
packages/types/src/workspace.d.ts
vendored
1
packages/types/src/workspace.d.ts
vendored
@@ -118,6 +118,7 @@ export interface IWorkspaceIssueSearchResult {
|
||||
project_id: string;
|
||||
sequence_id: number;
|
||||
workspace__slug: string;
|
||||
type_id: string;
|
||||
}
|
||||
|
||||
export interface IWorkspacePageSearchResult {
|
||||
|
||||
@@ -5,7 +5,6 @@ import useFontFaceObserver from "use-font-face-observer";
|
||||
import { LUCIDE_ICONS_LIST } from "./icons";
|
||||
// helpers
|
||||
import { emojiCodeToUnicode } from "./helpers";
|
||||
import { cn } from "../../helpers";
|
||||
|
||||
type TLogoProps = {
|
||||
in_use: "emoji" | "icon";
|
||||
@@ -23,11 +22,10 @@ type Props = {
|
||||
logo: TLogoProps;
|
||||
size?: number;
|
||||
type?: "lucide" | "material";
|
||||
customColor?: string;
|
||||
};
|
||||
|
||||
export const Logo: FC<Props> = (props) => {
|
||||
const { logo, size = 16, customColor, type = "material" } = props;
|
||||
const { logo, size = 16, type = "material" } = props;
|
||||
|
||||
// destructuring the logo object
|
||||
const { in_use, emoji, icon } = logo;
|
||||
@@ -74,20 +72,19 @@ export const Logo: FC<Props> = (props) => {
|
||||
{lucideIcon && (
|
||||
<lucideIcon.element
|
||||
style={{
|
||||
color: !customColor ? color : undefined,
|
||||
color: color,
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
className={cn(customColor)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className={cn("material-symbols-rounded", customColor)}
|
||||
className="material-symbols-rounded"
|
||||
style={{
|
||||
fontSize: size,
|
||||
color: !customColor ? color : undefined,
|
||||
color: color,
|
||||
scale: "115%",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"NEXT_PUBLIC_PLANE_ONE_PAYMENT_URL",
|
||||
"NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL",
|
||||
"NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL",
|
||||
"NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL"
|
||||
"NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL",
|
||||
"NEXT_PUBLIC_PRO_SELF_HOSTED_MONTHLY_PAYMENT_URL"
|
||||
],
|
||||
"tasks": {
|
||||
"build": {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
||||
<AuthenticationWrapper>
|
||||
<CommandPalette />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
|
||||
@@ -48,7 +48,7 @@ const ImportsPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full flex-col gap-10 rounded-xl px-8">
|
||||
<div className="flex h-full flex-col gap-10 rounded-xl md:pr-9 pr-4">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ const ImportsPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<section className="flex flex-col justify-start flex-grow h-full w-full overflow-y-auto py-8 pr-9">
|
||||
<section className="flex flex-col justify-start flex-grow h-full w-full overflow-y-auto md:pr-9 pr-4">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5 flex-shrink-0">
|
||||
<h3 className="text-xl font-medium">Imports</h3>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
import SiloIframe from "@/plane-web/components/iframe/silo-iframe";
|
||||
|
||||
const ImportProviderPage = observer(() => {
|
||||
const { workspaceSlug, providerSlug } = useParams();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { config } = useInstance();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (e: MessageEvent) => {
|
||||
if (e.data.msg === "created-migration") {
|
||||
router.push(`/${workspaceSlug}/settings/imports`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage, false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage, false);
|
||||
};
|
||||
}, [router, workspaceSlug]);
|
||||
|
||||
if (config?.silo_base_url) {
|
||||
return (
|
||||
<SiloIframe
|
||||
|
||||
@@ -51,7 +51,7 @@ const WorkspaceIntegrationsPage = observer(() => {
|
||||
|
||||
if (true)
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-10 rounded-xl px-8">
|
||||
<div className="flex h-full flex-col gap-10 rounded-xl md:pr-9 pr-4">
|
||||
<div className="flex items-center border-b border-custom-border-100 py-3.5">
|
||||
<h3 className="text-xl font-medium">Integrations</h3>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@ import "@/styles/react-day-picker.css";
|
||||
import { SITE_NAME, SITE_DESCRIPTION } from "@/constants/meta";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// plane web components
|
||||
import { FreeTrialBanner } from "@/plane-web/components/license/free-trial-banner";
|
||||
// local
|
||||
import { AppProvider } from "./provider";
|
||||
|
||||
@@ -70,7 +72,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<body>
|
||||
<div id="context-menu-portal" />
|
||||
<AppProvider>
|
||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>{children}</div>
|
||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100 relative flex flex-col`}>
|
||||
<div className="flex-shrink-0">
|
||||
{/* free trial banner */}
|
||||
<FreeTrialBanner />
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden">{children}</div>
|
||||
</div>
|
||||
</AppProvider>
|
||||
</body>
|
||||
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
|
||||
|
||||
@@ -18,24 +18,38 @@ import { PaymentService } from "@/plane-web/services/payment.service";
|
||||
const paymentService = new PaymentService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMonth: number): number => {
|
||||
const monthlyCost = monthlyPrice * 12;
|
||||
const yearlyCost = yearlyPricePerMonth * 12;
|
||||
const amountSaved = monthlyCost - yearlyCost;
|
||||
const discountPercentage = (amountSaved / monthlyCost) * 100;
|
||||
return Math.floor(discountPercentage);
|
||||
};
|
||||
|
||||
const renderPlanPricing = (price: number, members: number = 1, recurring: string) => {
|
||||
if (recurring === "month") return ((price / 100) * members).toFixed(0);
|
||||
if (recurring === "year") return ((price / 100) * members).toFixed(0);
|
||||
};
|
||||
|
||||
const CloudUpgradePlanPage = observer(() => {
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<IPaymentProductPrice | null>(null);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
// router
|
||||
const { selectedWorkspace } = useParams();
|
||||
|
||||
const { selectedWorkspace: workspaceSlug } = useParams();
|
||||
|
||||
// fetch workspace members
|
||||
const { data: workspaceMembers, isLoading: isFetching } = useSWR(
|
||||
selectedWorkspace ? `WORKSPACES_WITH_PLAN_DETAILS` : null,
|
||||
selectedWorkspace ? () => workspaceService.fetchWorkspaceMembers(selectedWorkspace.toString()) : null,
|
||||
workspaceSlug ? `CLOUD_PRO_WORKSPACE_MEMBER_DETAILS` : null,
|
||||
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch products
|
||||
const { data, isLoading: isLoadingProduct } = useSWR(
|
||||
selectedWorkspace ? "CLOUD_PAYMENT_PRODUCTS" : null,
|
||||
selectedWorkspace ? () => paymentService.listProducts(selectedWorkspace.toString()) : null,
|
||||
const { data: products, isLoading: isLoadingProduct } = useSWR(
|
||||
workspaceSlug ? "CLOUD_PRO_PRODUCTS" : null,
|
||||
workspaceSlug ? () => paymentService.listProducts(workspaceSlug.toString()) : null,
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
@@ -43,26 +57,29 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
const totalWorkspaceMembers = (workspaceMembers || [])?.filter((member) => member.role >= 15)?.length;
|
||||
// fetch workspace current plan
|
||||
const { data: workspaceSubscription, isLoading: workspaceSubscriptionLoading } = useSWR(
|
||||
workspaceSlug ? `CLOUD_PRO_WORKSPACE_SUBSCRIPTION_PLAN_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => paymentService.getWorkspaceCurrentPlan(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const proProduct = (data || [])?.find((product: IPaymentProduct) => product?.type === "PRO");
|
||||
// derived values
|
||||
const totalWorkspaceMembers = (workspaceMembers || [])?.filter((member) => member.role >= 15)?.length;
|
||||
const proProduct = (products || [])?.find((product: IPaymentProduct) => product?.type === "PRO");
|
||||
|
||||
const monthlyPlan = proProduct?.prices?.find((price) => price.recurring === "month");
|
||||
const yearlyPlan = proProduct?.prices?.find((price) => price.recurring === "year");
|
||||
|
||||
const calculateYearlyDiscount = (monthlyAmount: number | undefined, yearlyAmount: number | undefined) => {
|
||||
if (!monthlyAmount || !yearlyAmount) return undefined;
|
||||
const yearlyCostIfPaidMonthly = monthlyAmount * 12;
|
||||
const discountAmount = yearlyCostIfPaidMonthly - yearlyAmount;
|
||||
const discountPercentage = (discountAmount / yearlyCostIfPaidMonthly) * 100;
|
||||
const monthlyPlanUnitPrice = (monthlyPlan?.unit_amount || 0) / 100;
|
||||
const yearlyPlanUnitPrice = (yearlyPlan?.unit_amount || 0) / 1200;
|
||||
const yearlyDiscountedPrice = calculateYearlyDiscount(monthlyPlanUnitPrice, yearlyPlanUnitPrice);
|
||||
|
||||
return discountPercentage;
|
||||
};
|
||||
|
||||
const discountedPrice = calculateYearlyDiscount(monthlyPlan?.unit_amount, yearlyPlan?.unit_amount);
|
||||
const workspaceOnTrial =
|
||||
workspaceSubscription?.has_activated_free_trial && workspaceSubscription?.trial_end_date ? true : false;
|
||||
|
||||
const handleStripeCheckout = (priceId: string) => {
|
||||
if (!selectedWorkspace) {
|
||||
if (!workspaceSlug) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Please select a workspace to continue",
|
||||
@@ -71,7 +88,7 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
}
|
||||
setLoading(true);
|
||||
paymentService
|
||||
.getCurrentWorkspacePaymentLink(selectedWorkspace.toString(), {
|
||||
.getCurrentWorkspacePaymentLink(workspaceSlug.toString(), {
|
||||
price_id: priceId,
|
||||
product_id: proProduct?.id,
|
||||
})
|
||||
@@ -92,16 +109,46 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleTrialStripeCheckout = (priceId: string) => {
|
||||
if (!workspaceSlug) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Please select a workspace to continue",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
paymentService
|
||||
.modifyTrailSubscription(workspaceSlug.toString(), {
|
||||
price_id: priceId,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.session_url) {
|
||||
window.open(response.session_url, "_self");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.detail ?? "Failed to generate payment link. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 pb-4">
|
||||
<div className="text-3xl font-semibold">Choose your billing frequency</div>
|
||||
{discountedPrice && (
|
||||
<div className="text-center text-base text-custom-text-300">{`Upgrade to our yearly plan and get ${Math.round(discountedPrice)}% off.`}</div>
|
||||
{yearlyDiscountedPrice && (
|
||||
<div className="text-center text-base text-custom-text-300">{`Upgrade to our yearly plan and get ${Math.round(yearlyDiscountedPrice)}% off.`}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFetching || isLoadingProduct ? (
|
||||
{isFetching || isLoadingProduct || workspaceSubscriptionLoading ? (
|
||||
<div className="flex flex-col rounded gap-1 w-full">
|
||||
<Loader.Item height="90px" />
|
||||
<Loader.Item height="90px" />
|
||||
@@ -132,7 +179,9 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
<div className="flex flex-col">
|
||||
{totalWorkspaceMembers && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-semibold leading-7">{`$${plan.unit_amount / 100}`}</span>
|
||||
<span className="text-2xl font-semibold leading-7">
|
||||
${renderPlanPricing(plan.unit_amount, 1, plan.recurring)}
|
||||
</span>
|
||||
<span className="text-sm text-custom-text-300">
|
||||
{` per user per ${plan.recurring} x ${totalWorkspaceMembers}`}
|
||||
</span>
|
||||
@@ -143,10 +192,11 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col text-right">
|
||||
{totalWorkspaceMembers && (
|
||||
<span className="text-2xl font-semibold leading-7">{`$${(plan.unit_amount / 100) * totalWorkspaceMembers}`}</span>
|
||||
<span className="text-2xl font-semibold leading-7">
|
||||
${renderPlanPricing(plan.unit_amount, totalWorkspaceMembers, plan.recurring)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-custom-text-300">
|
||||
{plan.recurring === "month" ? "per month" : "per year"}
|
||||
@@ -156,12 +206,18 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlan?.recurring && selectedPlan.unit_amount ? (
|
||||
<>
|
||||
<Button className="w-full px-2 my-4" onClick={() => handleStripeCheckout(selectedPlan.id)}>
|
||||
<Button
|
||||
className="w-full px-2 my-4"
|
||||
onClick={() =>
|
||||
workspaceOnTrial ? handleTrialStripeCheckout(selectedPlan.id) : handleStripeCheckout(selectedPlan.id)
|
||||
}
|
||||
>
|
||||
{isLoading
|
||||
? "Redirecting to Stripe..."
|
||||
: `Pay $${(selectedPlan?.unit_amount / 100) * totalWorkspaceMembers} every ${selectedPlan?.recurring}`}
|
||||
: `Pay $${renderPlanPricing(selectedPlan.unit_amount, totalWorkspaceMembers, selectedPlan.recurring)} every ${selectedPlan?.recurring}`}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -18,6 +18,8 @@ import { useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// plane web types
|
||||
import { TWorkspaceWithProductDetails } from "@/plane-web/types/workspace";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
@@ -33,7 +35,7 @@ const CloudUpgradePage = observer(() => {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { data: workspacesList, isLoading: isFetching } = useSWR(
|
||||
currentUser ? `WORKSPACES_WITH_PLAN_DETAILS` : null,
|
||||
currentUser ? `CLOUD_PRO_WORKSPACES_LIST` : null,
|
||||
currentUser ? () => workspaceService.getWorkspacesWithPlanDetails() : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
@@ -69,6 +71,23 @@ const CloudUpgradePage = observer(() => {
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
const workspaceSubscription = (workspace: TWorkspaceWithProductDetails): "FREE" | "PRO" | "TRIAL" => {
|
||||
switch (workspace.product) {
|
||||
case "PRO":
|
||||
if (workspace.is_offline_payment) {
|
||||
return "PRO";
|
||||
} else if (workspace.trial_end_date && workspace.has_added_payment_method) {
|
||||
return "PRO";
|
||||
} else if (workspace.trial_end_date && !workspace.has_added_payment_method) {
|
||||
return "TRIAL";
|
||||
}
|
||||
case "FREE":
|
||||
return "FREE";
|
||||
default:
|
||||
return "FREE";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-2">
|
||||
<RadioInput
|
||||
@@ -111,7 +130,9 @@ const CloudUpgradePage = observer(() => {
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{truncateText(workspace?.name, 40)}</div>
|
||||
</div>
|
||||
{workspace.product === "PRO" && (
|
||||
|
||||
{/* pro */}
|
||||
{workspaceSubscription(workspace) === "PRO" && (
|
||||
<div className="flex-shrink-0">
|
||||
<Tooltip position="right" tooltipContent="This workspace is already subscribed to Pro.">
|
||||
<div
|
||||
@@ -124,10 +145,25 @@ const CloudUpgradePage = observer(() => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* trial */}
|
||||
{workspaceSubscription(workspace) === "TRIAL" && (
|
||||
<div className="flex-shrink-0">
|
||||
<Tooltip position="right" tooltipContent="This workspace is in Trial please upgrade to Pro.">
|
||||
<div
|
||||
className={cn(
|
||||
"text-custom-primary-100 bg-custom-primary-100/20 rounded-md px-2 py-0 text-center text-xs font-medium outline-none"
|
||||
)}
|
||||
>
|
||||
Trial
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: workspace.slug,
|
||||
disabled: workspace.product !== "FREE",
|
||||
disabled: ["PRO"].includes(workspaceSubscription(workspace)),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -44,9 +44,7 @@ export default function UpgradePlanPage() {
|
||||
router.push("/upgrade/pro/cloud");
|
||||
}
|
||||
if (selectedEdition === "self-hosted") {
|
||||
if (process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL) {
|
||||
window.open(process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL, "_blank");
|
||||
}
|
||||
router.push("/upgrade/pro/self-hosted");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
24
web/app/upgrade/pro/self-hosted/layout.tsx
Normal file
24
web/app/upgrade/pro/self-hosted/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function SelfHostedUpgradeLayout(props: Props) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<PageHead title="Self-hosted upgrade - Plane" />
|
||||
<AuthenticationWrapper pageType={EPageTypes.PUBLIC}>{children}</AuthenticationWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
102
web/app/upgrade/pro/self-hosted/page.tsx
Normal file
102
web/app/upgrade/pro/self-hosted/page.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// ui
|
||||
import { Button, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
type TSelfHostedProduct = {
|
||||
recurring: "month" | "year";
|
||||
unit_amount: number;
|
||||
redirection_link: string | undefined;
|
||||
};
|
||||
|
||||
const selfHostedProducts: TSelfHostedProduct[] = [
|
||||
{
|
||||
recurring: "month",
|
||||
unit_amount: 8,
|
||||
redirection_link: process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_MONTHLY_PAYMENT_URL || undefined,
|
||||
},
|
||||
{
|
||||
recurring: "year",
|
||||
unit_amount: 72,
|
||||
redirection_link: process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL || undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const SelfHostedUpgradePlanPage = observer(() => {
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<TSelfHostedProduct | undefined>(undefined);
|
||||
|
||||
const handleStripeRedirection = () => {
|
||||
if (selectedPlan?.redirection_link) {
|
||||
window.open(selectedPlan.redirection_link, "_blank");
|
||||
} else {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to generate payment link.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 pb-4">
|
||||
<div className="text-3xl font-semibold">Choose your billing frequency</div>
|
||||
<div className="text-center text-base text-custom-text-300">Upgrade to our yearly plan and get 25% off.</div>
|
||||
</div>
|
||||
<div className="flex flex-col rounded w-full">
|
||||
{selfHostedProducts &&
|
||||
selfHostedProducts.map((product) => (
|
||||
<div
|
||||
key={product.unit_amount}
|
||||
className={cn(
|
||||
"flex items-center gap-6 border-x border border-custom-border-200 cursor-pointer rounded py-6 px-4 first:rounded-b-none last:rounded-t-none",
|
||||
{
|
||||
"border border-custom-primary-100": product.recurring === selectedPlan?.recurring,
|
||||
}
|
||||
)}
|
||||
onClick={() => setSelectedPlan(product)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<span
|
||||
className={cn("flex items-center justify-center size-6 rounded-full", {
|
||||
"bg-custom-primary-100 text-white": product.recurring === selectedPlan?.recurring,
|
||||
"border border-custom-border-200": product.recurring !== selectedPlan?.recurring,
|
||||
})}
|
||||
>
|
||||
{product.recurring === selectedPlan?.recurring && <Check className="size-4 stroke-2" />}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-semibold leading-7">${product.unit_amount}</span>
|
||||
<span className="text-sm text-custom-text-300">{` per user per ${product.recurring}`}</span>
|
||||
</div>
|
||||
<span className="text-sm text-custom-text-300">
|
||||
{`Billed ${product.recurring === "month" ? "monthly" : "yearly"}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedPlan?.recurring && selectedPlan.unit_amount ? (
|
||||
<>
|
||||
<Button className="w-full px-2 my-4" onClick={handleStripeRedirection}>
|
||||
{`Pay $${selectedPlan.unit_amount} every ${selectedPlan?.recurring}`}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button className="w-full px-2 my-4" disabled>{`Select a plan to continue`}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default SelfHostedUpgradePlanPage;
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./issue-identifier";
|
||||
export * from "./issue-properties-activity";
|
||||
|
||||
@@ -4,29 +4,41 @@ import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
|
||||
type TIssueIdentifierProps = {
|
||||
issueId: string;
|
||||
type TIssueIdentifierBaseProps = {
|
||||
projectId: string;
|
||||
iconSize?: number;
|
||||
iconContainerSize?: number;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
textContainerClassName?: string;
|
||||
};
|
||||
|
||||
type TIssueIdentifierFromStore = TIssueIdentifierBaseProps & {
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
type TIssueIdentifierWithDetails = TIssueIdentifierBaseProps & {
|
||||
issueTypeId?: string | null;
|
||||
projectIdentifier: string;
|
||||
issueSequenceId: string | number;
|
||||
};
|
||||
|
||||
type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails;
|
||||
export const IssueIdentifier: React.FC<TIssueIdentifierProps> = observer((props) => {
|
||||
const { issueId, projectId, textContainerClassName } = props;
|
||||
const { projectId, textContainerClassName } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
// Determine if the component is using store data or not
|
||||
const isUsingStoreData = "issueId" in props;
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const issue = isUsingStoreData ? getIssueById(props.issueId) : null;
|
||||
const projectIdentifier = isUsingStoreData ? getProjectIdentifierById(projectId) : props.projectIdentifier;
|
||||
const issueSequenceId = isUsingStoreData ? issue?.sequence_id : props.issueSequenceId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={cn("text-xs font-medium text-custom-text-300", textContainerClassName)}>
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
<span className={cn("text-base font-medium text-custom-text-300", textContainerClassName)}>
|
||||
{projectIdentifier}-{issueSequenceId}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./root";
|
||||
@@ -0,0 +1,8 @@
|
||||
import { FC } from "react";
|
||||
|
||||
type TIssueAdditionalPropertiesActivity = {
|
||||
activityId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
};
|
||||
|
||||
export const IssueAdditionalPropertiesActivity: FC<TIssueAdditionalPropertiesActivity> = () => <></>;
|
||||
@@ -7,10 +7,11 @@ import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { FolderPlus, Search, Settings } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
// types
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
ChangeIssueAssignee,
|
||||
ChangeIssuePriority,
|
||||
@@ -23,8 +24,11 @@ import {
|
||||
CommandPaletteWorkspaceSettingsActions,
|
||||
} from "@/components/command-palette";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
// fetch-keys
|
||||
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
@@ -36,12 +40,6 @@ import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { IssueService } from "@/services/issue";
|
||||
|
||||
// ui
|
||||
// components
|
||||
// types
|
||||
// fetch-keys
|
||||
// constants
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const issueService = new IssueService();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IWorkspaceProjectSearchResult,
|
||||
IWorkspaceSearchResult,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { ContrastIcon, DiceIcon } from "@plane/ui";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
@@ -37,10 +38,10 @@ export const commandGroups: {
|
||||
itemName: (issue: IWorkspaceIssueSearchResult) => (
|
||||
<div className="flex gap-2">
|
||||
<IssueIdentifier
|
||||
issueId={issue.id}
|
||||
projectId={issue.project_id}
|
||||
iconSize={10}
|
||||
iconContainerSize={16}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs"
|
||||
/>{" "}
|
||||
{issue.name}
|
||||
|
||||
@@ -165,7 +165,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Users2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Users2Icon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: (activity) => {
|
||||
@@ -182,7 +182,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <ArchiveIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <ArchiveIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
attachment: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -219,7 +219,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <PaperclipIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
description: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -233,7 +233,7 @@ const activityDetails: {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <MessageSquareIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
estimate_point: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -262,7 +262,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <TriangleIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <TriangleIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
issue: {
|
||||
message: (activity) => {
|
||||
@@ -279,7 +279,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <LayersIcon width={12} height={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
labels: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
@@ -320,7 +320,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <TagIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <TagIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
link: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -385,7 +385,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Link2Icon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
cycles: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
@@ -435,7 +435,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <ContrastIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
modules: {
|
||||
message: (activity, showIssue, workspaceSlug) => {
|
||||
@@ -482,7 +482,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <DiceIcon className="h-3 w-3 !text-[#6b7280]" aria-hidden="true" />,
|
||||
icon: <DiceIcon className="h-3 w-3 !text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -496,7 +496,7 @@ const activityDetails: {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <MessageSquareIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
parent: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -527,7 +527,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <UsersIcon className="h-3 w-3 !text-[#6b7280]" aria-hidden="true" />,
|
||||
icon: <UsersIcon className="h-3 w-3 !text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
priority: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -544,7 +544,7 @@ const activityDetails: {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <SignalMediumIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
relates_to: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -563,7 +563,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <RelatedIcon height="12" width="12" color="#6b7280" />,
|
||||
icon: <RelatedIcon height="12" width="12" className="text-custom-text-200" />,
|
||||
},
|
||||
blocking: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -582,7 +582,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
|
||||
icon: <BlockerIcon height="12" width="12" className="text-custom-text-200" />,
|
||||
},
|
||||
blocked_by: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -601,7 +601,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||
icon: <BlockedIcon height="12" width="12" className="text-custom-text-200" />,
|
||||
},
|
||||
duplicate: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -620,7 +620,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <CopyPlus size={12} color="#6b7280" />,
|
||||
icon: <CopyPlus size={12} className="text-custom-text-200" />,
|
||||
},
|
||||
state: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -634,7 +634,7 @@ const activityDetails: {
|
||||
)}
|
||||
</>
|
||||
),
|
||||
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <LayoutGridIcon size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
start_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -666,7 +666,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Calendar size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
target_date: {
|
||||
message: (activity, showIssue) => {
|
||||
@@ -697,7 +697,7 @@ const activityDetails: {
|
||||
</>
|
||||
);
|
||||
},
|
||||
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Calendar size={12} className="text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
inbox: {
|
||||
message: (activity, showIssue) => (
|
||||
@@ -712,7 +712,7 @@ const activityDetails: {
|
||||
{activity.verb === "2" && ` from intake by marking a duplicate issue.`}
|
||||
</>
|
||||
),
|
||||
icon: <Intake className="size-3" color="#6b7280" aria-hidden="true" />,
|
||||
icon: <Intake className="size-3 text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -734,7 +734,7 @@ export const ActivityMessage = ({ activity, showIssue = false }: ActivityMessage
|
||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||
activity,
|
||||
showIssue,
|
||||
workspaceSlug ? workspaceSlug.toString() : activity.workspace_detail?.slug ?? ""
|
||||
workspaceSlug ? workspaceSlug.toString() : (activity.workspace_detail?.slug ?? "")
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// hooks
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
// plane web hooks
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
interface Props {
|
||||
@@ -20,7 +21,7 @@ export const BulkDeleteIssuesModalItem: React.FC<Props> = observer((props: Props
|
||||
as="div"
|
||||
value={issue.id}
|
||||
className={({ active }) =>
|
||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 ${
|
||||
`flex cursor-pointer select-none items-center justify-between rounded-md px-3 py-2 my-0.5 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
}`
|
||||
}
|
||||
|
||||
@@ -6,22 +6,21 @@ import { useParams } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { Search } from "lucide-react";
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
//plane
|
||||
// types
|
||||
import { ISearchIssueResponse, IUser } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
//components
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
//constants
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
//hooks
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store";
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
// ui
|
||||
// icons
|
||||
// components
|
||||
// local components
|
||||
import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item";
|
||||
|
||||
type FormInput = {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@pl
|
||||
// hooks
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
// components
|
||||
@@ -149,7 +151,13 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
key={issue.id}
|
||||
className="flex items-center gap-1 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 py-1 pl-2 text-xs text-custom-text-100"
|
||||
>
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
<IssueIdentifier
|
||||
projectId={issue.project_id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="group p-1"
|
||||
@@ -232,7 +240,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
htmlFor={`issue-${issue.id}`}
|
||||
value={issue}
|
||||
className={({ active }) =>
|
||||
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
@@ -245,8 +253,14 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
backgroundColor: issue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs">
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
<span className="flex-shrink-0">
|
||||
<IssueIdentifier
|
||||
projectId={issue.project_id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</span>
|
||||
{issue.name}
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
import isToday from "date-fns/isToday";
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { TIssue, TWidgetIssue } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, ControlLink, PriorityIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { findTotalDaysInRange, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useMember, useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
|
||||
export type IssueListItemProps = {
|
||||
issueId: string;
|
||||
@@ -67,12 +67,12 @@ export const AssignedUpcomingIssueListItem: React.FC<IssueListItemProps> = obser
|
||||
? blockedByIssues.length > 1
|
||||
? `${blockedByIssues.length} blockers`
|
||||
: blockedByIssueProjectDetails && (
|
||||
<IssueIdentifier
|
||||
issueId={blockedByIssues[0]?.id}
|
||||
projectId={blockedByIssueProjectDetails?.id}
|
||||
textContainerClassName="text-xs text-custom-text-200 font-medium"
|
||||
/>
|
||||
)
|
||||
<IssueIdentifier
|
||||
issueId={blockedByIssues[0]?.id}
|
||||
projectId={blockedByIssueProjectDetails?.id}
|
||||
textContainerClassName="text-xs text-custom-text-200 font-medium"
|
||||
/>
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</ControlLink>
|
||||
@@ -126,12 +126,12 @@ export const AssignedOverdueIssueListItem: React.FC<IssueListItemProps> = observ
|
||||
? blockedByIssues.length > 1
|
||||
? `${blockedByIssues.length} blockers`
|
||||
: blockedByIssueProjectDetails && (
|
||||
<IssueIdentifier
|
||||
issueId={blockedByIssues[0]?.id}
|
||||
projectId={blockedByIssueProjectDetails?.id}
|
||||
textContainerClassName="text-xs text-custom-text-200 font-medium"
|
||||
/>
|
||||
)
|
||||
<IssueIdentifier
|
||||
issueId={blockedByIssues[0]?.id}
|
||||
projectId={blockedByIssueProjectDetails?.id}
|
||||
textContainerClassName="text-xs text-custom-text-200 font-medium"
|
||||
/>
|
||||
)
|
||||
: "-"}
|
||||
</div>
|
||||
</ControlLink>
|
||||
|
||||
@@ -74,23 +74,23 @@ export const WidgetIssuesList: React.FC<WidgetIssuesListProps> = (props) => {
|
||||
</Loader>
|
||||
) : issuesList.length > 0 ? (
|
||||
<>
|
||||
<div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-12 gap-1 text-xs text-custom-text-300 pb-1">
|
||||
<div className="mt-7 mx-6 border-b-[0.5px] border-custom-border-200 grid grid-cols-12 gap-1 text-xs text-custom-text-300 pb-1">
|
||||
<h6
|
||||
className={cn("pl-1 flex items-center gap-1 col-span-7", {
|
||||
"col-span-11": type === "assigned" && tab === "completed",
|
||||
"col-span-9": type === "created" && tab === "completed",
|
||||
className={cn("pl-1 flex items-center gap-1 col-span-7", {
|
||||
"col-span-11": type === "assigned" && tab === "completed",
|
||||
"col-span-9": type === "created" && tab === "completed",
|
||||
})}
|
||||
>
|
||||
Issues
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-2 flex items-center text-center justify-center">
|
||||
<span className="flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-medium rounded-xl px-2 flex items-center text-center justify-center">
|
||||
{widgetStats.count}
|
||||
</span>
|
||||
</h6>
|
||||
<h6 className="text-center col-span-1">Priority</h6>
|
||||
{["upcoming", "pending"].includes(tab) && <h6 className="text-center col-span-2">Due date</h6>}
|
||||
{tab === "overdue" && <h6 className="text-center col-span-2">Due by</h6>}
|
||||
{type === "assigned" && tab !== "completed" && <h6 className="text-center col-span-2">Blocked by</h6>}
|
||||
{type === "created" && <h6 className="text-center col-span-2">Assigned to</h6>}
|
||||
<h6 className="text-center col-span-1">Priority</h6>
|
||||
{["upcoming", "pending"].includes(tab) && <h6 className="text-center col-span-2">Due date</h6>}
|
||||
{tab === "overdue" && <h6 className="text-center col-span-2">Due by</h6>}
|
||||
{type === "assigned" && tab !== "completed" && <h6 className="text-center col-span-2">Blocked by</h6>}
|
||||
{type === "created" && <h6 className="text-center col-span-2">Assigned to</h6>}
|
||||
</div>
|
||||
<div className="px-4 pb-3 mt-2">
|
||||
{issuesList.map((issue) => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// plane wev components
|
||||
// plane web components
|
||||
import { IssueAdditionalPropertiesActivity } from "@/plane-web/components/issues";
|
||||
import { IssueActivityWorklog } from "@/plane-web/components/issues/worklog/activity/root";
|
||||
// plane web constants
|
||||
import { TActivityFilters, filterActivityOnSelectedFilters } from "@/plane-web/constants/issues";
|
||||
@@ -56,6 +57,11 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
|
||||
activityId={activityComment.id}
|
||||
ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined}
|
||||
/>
|
||||
) : activityComment.activity_type === "ISSUE_ADDITIONAL_PROPERTIES_ACTIVITY" ? (
|
||||
<IssueAdditionalPropertiesActivity
|
||||
activityId={activityComment.id}
|
||||
ends={index === 0 ? "top" : index === filteredActivityComments.length - 1 ? "bottom" : undefined}
|
||||
/>
|
||||
) : activityComment.activity_type === "WORKLOG" ? (
|
||||
<IssueActivityWorklog
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
||||
@@ -27,9 +27,9 @@ export const IssueArchivedAtActivity: FC<TIssueArchivedAtActivity> = observer((p
|
||||
<IssueActivityBlockComponent
|
||||
icon={
|
||||
activity.new_value === "restore" ? (
|
||||
<RotateCcw className="h-3.5 w-3.5" color="#6b7280" aria-hidden="true" />
|
||||
<RotateCcw className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
) : (
|
||||
<ArchiveIcon className="h-3.5 w-3.5" color="#6b7280" aria-hidden="true" />
|
||||
<ArchiveIcon className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
)
|
||||
}
|
||||
activityId={activityId}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IssueAssigneeActivity: FC<TIssueAssigneeActivity> = observer((props
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Users className="h-3 w-3 flex-shrink-0" />}
|
||||
icon={<Users className="h-3.5 w-3.5 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssueAttachmentActivity: FC<TIssueAttachmentActivity> = observer((p
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Paperclip size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<Paperclip size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IssueCycleActivity: FC<TIssueCycleActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<ContrastIcon className="h-4 w-4 flex-shrink-0 text-[#6b7280]" />}
|
||||
icon={<ContrastIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props)
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
activityId={activityId}
|
||||
icon={<LayersIcon width={12} height={12} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
ends={ends}
|
||||
>
|
||||
<>{activity.verb === "created" ? " created the issue." : " deleted an issue."}</>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssueDescriptionActivity: FC<TIssueDescriptionActivity> = observer(
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<MessageSquare size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IssueEstimateActivity: FC<TIssueEstimateActivity> = observer((props
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Triangle size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<Triangle size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const IssueInboxActivity: FC<TIssueInboxActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Intake className="h-4 w-4 flex-shrink-0" />}
|
||||
icon={<Intake className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IssueLabelActivity: FC<TIssueLabelActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Tag size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<Tag size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssueLinkActivity: FC<TIssueLinkActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<MessageSquare size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IssueModuleActivity: FC<TIssueModuleActivity> = observer((props) =>
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<DiceIcon className="h-4 w-4 flex-shrink-0 text-[#6b7280]" />}
|
||||
icon={<DiceIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssueNameActivity: FC<TIssueNameActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<MessageSquare size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssueParentActivity: FC<TIssueParentActivity> = observer((props) =>
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<LayoutPanelTop size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<LayoutPanelTop size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const IssuePriorityActivity: FC<TIssuePriorityActivity> = observer((props
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<Signal size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<Signal size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const IssueStartDateActivity: FC<TIssueStartDateActivity> = observer((pro
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<CalendarDays size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<CalendarDays size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IssueStateActivity: FC<TIssueStateActivity> = observer((props) => {
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />}
|
||||
icon={<DoubleCircleIcon className="h-4 w-4 flex-shrink-0 text-custom-text-200" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const IssueTargetDateActivity: FC<TIssueTargetDateActivity> = observer((p
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<CalendarDays size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={<CalendarDays size={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
>
|
||||
|
||||
@@ -45,7 +45,7 @@ export const IssueCommentBlock: FC<TIssueCommentBlock> = observer((props) => {
|
||||
</>
|
||||
)}
|
||||
<div className="absolute top-2 left-4 w-5 h-5 rounded-full overflow-hidden flex justify-center items-center bg-custom-background-80">
|
||||
<MessageCircle className="w-3 h-3" color="#6b7280" />
|
||||
<MessageCircle className="w-3 h-3 text-custom-text-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full truncate relative flex ">
|
||||
|
||||
@@ -67,7 +67,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
<div className="mb-2.5 flex items-center gap-4">
|
||||
<IssueIdentifier issueId={issueId} projectId={issue.project_id} iconSize={14} iconContainerSize={20} />
|
||||
<IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" />
|
||||
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,15 +4,17 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Pencil, X } from "lucide-react";
|
||||
// hooks
|
||||
// components
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
import { ParentIssuesListModal } from "@/components/issues";
|
||||
// ui
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ParentIssuesListModal } from "@/components/issues";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
import { TIssueOperations } from "./root";
|
||||
|
||||
@@ -102,16 +104,23 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
disabled={disabled}
|
||||
>
|
||||
{issue.parent_id && parentIssue ? (
|
||||
<div className="flex items-center gap-1 bg-green-500/20 text-green-700 rounded px-1.5 py-1">
|
||||
<div className="flex items-center gap-1 bg-green-500/20 rounded px-1.5 py-1">
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={parentIssue.name} isMobile={isMobile}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${parentIssue.project_id}/issues/${parentIssue?.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{parentIssueProjectDetails?.identifier}-{parentIssue.sequence_id}
|
||||
{parentIssue?.project_id && parentIssueProjectDetails && (
|
||||
<IssueIdentifier
|
||||
projectId={parentIssue.project_id}
|
||||
issueTypeId={parentIssue.type_id}
|
||||
projectIdentifier={parentIssueProjectDetails?.identifier}
|
||||
issueSequenceId={parentIssue.sequence_id}
|
||||
textContainerClassName="text-xs font-medium text-green-700"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues, useProject, useProjectState } from "@/hooks/store";
|
||||
import { useIssues, useProjectState } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
import { TIssueOperations } from "../root";
|
||||
import { IssueParentSiblings } from "./siblings";
|
||||
@@ -26,7 +28,6 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issue, issueOperations } = props;
|
||||
// hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { getProjectById } = useProject();
|
||||
const { getProjectStates } = useProjectState();
|
||||
|
||||
const parentIssue = issueMap?.[issue.parent_id || ""] || undefined;
|
||||
@@ -42,12 +43,16 @@ export const IssueParentDetail: FC<TIssueParentDetail> = observer((props) => {
|
||||
<>
|
||||
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
||||
<Link href={`/${workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />
|
||||
<span className="flex-shrink-0 text-custom-text-200">
|
||||
{getProjectById(parentIssue.project_id)?.identifier}-{parentIssue?.sequence_id}
|
||||
</span>
|
||||
{parentIssue.project_id && (
|
||||
<IssueIdentifier
|
||||
projectId={parentIssue.project_id}
|
||||
issueId={parentIssue.id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate text-custom-text-100">{(parentIssue?.name ?? "").substring(0, 50)}</span>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,11 @@ import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
// ui
|
||||
import { CustomMenu, LayersIcon } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
type TIssueParentSiblingItem = {
|
||||
workspaceSlug: string;
|
||||
@@ -31,10 +33,17 @@ export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = observer((pro
|
||||
<CustomMenu.MenuItem key={issueDetail.id}>
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`}
|
||||
className="flex items-center gap-2 py-2"
|
||||
className="flex items-center gap-2 py-0.5"
|
||||
>
|
||||
<LayersIcon className="h-4 w-4" />
|
||||
{projectDetails?.identifier}-{issueDetail.sequence_id}
|
||||
{issueDetail.project_id && projectDetails?.identifier && (
|
||||
<IssueIdentifier
|
||||
projectId={issueDetail.project_id}
|
||||
issueTypeId={issueDetail.type_id}
|
||||
projectIdentifier={projectDetails?.identifier}
|
||||
issueSequenceId={issueDetail.sequence_id}
|
||||
textContainerClassName="text-xs"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const IssueParentSiblings: FC<TIssueParentSiblings> = observer((props) =>
|
||||
const subIssueIds = (parentIssue && subIssuesByIssueId(parentIssue.id)) || undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="my-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
|
||||
Loading
|
||||
|
||||
@@ -21,22 +21,22 @@ export type TRelationObject = { className: string; icon: (size: number) => React
|
||||
export const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
relates_to: {
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <RelatedIcon height={size} width={size} />,
|
||||
icon: (size) => <RelatedIcon height={size} width={size} className="text-custom-text-200" />,
|
||||
placeholder: "Add related issues",
|
||||
},
|
||||
blocking: {
|
||||
className: "bg-yellow-500/20 text-yellow-700",
|
||||
icon: (size) => <XCircle size={size} />,
|
||||
icon: (size) => <XCircle size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
blocked_by: {
|
||||
className: "bg-red-500/20 text-red-700",
|
||||
icon: (size) => <CircleDot size={size} />,
|
||||
icon: (size) => <CircleDot size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
duplicate: {
|
||||
className: "bg-custom-background-80 text-custom-text-200",
|
||||
icon: (size) => <CopyPlus size={size} />,
|
||||
icon: (size) => <CopyPlus size={size} className="text-custom-text-200" />,
|
||||
placeholder: "None",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,17 +5,19 @@ import { useState, useRef, forwardRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
// ui
|
||||
import { Tooltip, ControlLink } from "@plane/ui";
|
||||
// hooks
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
// helpers
|
||||
// types
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details";
|
||||
// local components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
// ui
|
||||
import { Tooltip, ControlLink } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -5,25 +5,26 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||
// hooks
|
||||
// ui
|
||||
import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// components
|
||||
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
|
||||
import { HIGHLIGHT_CLASS } from "@/components/issues/issue-layouts/utils";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useKanbanView } from "@/hooks/store";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { IssueProperties } from "../properties/all-properties";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { getIssueBlockId } from "../utils";
|
||||
// ui
|
||||
// types
|
||||
// helper
|
||||
|
||||
interface IssueBlockProps {
|
||||
issueId: string;
|
||||
|
||||
@@ -19,8 +19,9 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useAppTheme, useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// types
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
|
||||
@@ -20,8 +20,9 @@ import { useIssueDetail, useProject } from "@/hooks/store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local components
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { IssueColumn } from "./issue-column";
|
||||
|
||||
@@ -25,6 +25,8 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
|
||||
import { getTabIndex } from "@/helpers/issue-modal.helper";
|
||||
// hooks
|
||||
import { useProjectEstimates, useProject } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
type TIssueDefaultPropertiesProps = {
|
||||
control: Control<TIssue>;
|
||||
@@ -256,10 +258,15 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue && `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`}
|
||||
</span>
|
||||
{selectedParentIssue?.project_id && (
|
||||
<IssueIdentifier
|
||||
projectId={selectedParentIssue.project_id}
|
||||
issueTypeId={selectedParentIssue.type_id}
|
||||
projectIdentifier={selectedParentIssue?.project__identifier}
|
||||
issueSequenceId={selectedParentIssue.sequence_id}
|
||||
textContainerClassName="text-xs"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
|
||||
@@ -8,6 +8,8 @@ import { X } from "lucide-react";
|
||||
import { ISearchIssueResponse, TIssue } from "@plane/types";
|
||||
// helpers
|
||||
import { getTabIndex } from "@/helpers/issue-modal.helper";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
type TIssueParentTagProps = {
|
||||
control: Control<TIssue>;
|
||||
@@ -33,7 +35,15 @@ export const IssueParentTag: React.FC<TIssueParentTagProps> = observer((props) =
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-custom-text-200">
|
||||
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||
{selectedParentIssue?.project_id && (
|
||||
<IssueIdentifier
|
||||
projectId={selectedParentIssue.project_id}
|
||||
issueTypeId={selectedParentIssue.type_id}
|
||||
projectIdentifier={selectedParentIssue?.project__identifier}
|
||||
issueSequenceId={selectedParentIssue.sequence_id}
|
||||
textContainerClassName="text-xs"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate font-medium">{selectedParentIssue.name.substring(0, 50)}</span>
|
||||
<button
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// headless ui
|
||||
// icons
|
||||
import { Rocket, Search } from "lucide-react";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
// types
|
||||
import { ISearchIssueResponse } from "@plane/types";
|
||||
// ui
|
||||
import { Loader, ToggleSwitch, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { IssueSearchModalEmptyState } from "@/components/core";
|
||||
// hooks
|
||||
import useDebounce from "@/hooks/use-debounce";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
// hooks
|
||||
// components
|
||||
// ui
|
||||
// icons
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -182,7 +184,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
key={issue.id}
|
||||
value={issue}
|
||||
className={({ active, selected }) =>
|
||||
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
|
||||
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 my-0.5 text-custom-text-200 ${
|
||||
active ? "bg-custom-background-80 text-custom-text-100" : ""
|
||||
} ${selected ? "text-custom-text-100" : ""}`
|
||||
}
|
||||
@@ -194,8 +196,14 @@ export const ParentIssuesListModal: React.FC<Props> = ({
|
||||
backgroundColor: issue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs">
|
||||
{issue.project__identifier}-{issue.sequence_id}
|
||||
<span className="flex-shrink-0">
|
||||
<IssueIdentifier
|
||||
projectId={issue.project_id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={issue.project__identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
</span>{" "}
|
||||
<span className="truncate">{issue.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<IssueIdentifier issueId={issueId} projectId={issue.project_id} iconSize={14} iconContainerSize={20} />
|
||||
<IssueIdentifier issueId={issueId} projectId={issue.project_id} size="md" />
|
||||
<IssueTitleInput
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={issue.project_id}
|
||||
|
||||
@@ -241,7 +241,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 overflow-hidden vertical-scrollbar scrollbar-sm ${
|
||||
className={`h-full !w-[400px] flex-shrink-0 border-l border-custom-border-200 p-4 py-5 overflow-hidden vertical-scrollbar scrollbar-sm ${
|
||||
is_archived ? "pointer-events-none" : ""
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { RelationIssueProperty } from "@/components/issues/relations";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// types
|
||||
import { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
|
||||
|
||||
@@ -91,15 +93,23 @@ export const RelationIssueListItem: FC<Props> = observer((props) => {
|
||||
{issue && (
|
||||
<div className="group relative flex min-h-11 h-full w-full items-center gap-3 px-1.5 py-1 transition-all hover:bg-custom-background-90">
|
||||
<span className="size-5 flex-shrink-0" />
|
||||
<div className="flex w-full cursor-pointer items-center gap-2">
|
||||
<div className="flex w-full cursor-pointer items-center gap-3">
|
||||
<div
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: currentIssueStateDetail?.color ?? "#737373",
|
||||
}}
|
||||
/>
|
||||
<div className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{projectDetail?.identifier}-{issue?.sequence_id}
|
||||
<div className="flex-shrink-0">
|
||||
{projectDetail && (
|
||||
<IssueIdentifier
|
||||
projectId={projectDetail.id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={projectDetail.identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ControlLink
|
||||
|
||||
@@ -4,11 +4,16 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// components
|
||||
// ui
|
||||
import { ControlLink, CustomMenu, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { IssueList } from "./issues-list";
|
||||
import { IssueProperty } from "./properties";
|
||||
// ui
|
||||
@@ -117,15 +122,23 @@ export const IssueListItem: React.FC<ISubIssues> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full cursor-pointer items-center gap-2">
|
||||
<div className="flex w-full cursor-pointer items-center gap-3">
|
||||
<div
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: currentIssueStateDetail?.color ?? "#737373",
|
||||
}}
|
||||
/>
|
||||
<div className="flex-shrink-0 text-xs text-custom-text-200">
|
||||
{projectDetail?.identifier}-{issue?.sequence_id}
|
||||
<div className="flex-shrink-0">
|
||||
{projectDetail && (
|
||||
<IssueIdentifier
|
||||
projectId={projectDetail.id}
|
||||
issueTypeId={issue.type_id}
|
||||
projectIdentifier={projectDetail.identifier}
|
||||
issueSequenceId={issue.sequence_id}
|
||||
textContainerClassName="text-xs text-custom-text-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ControlLink
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
useUser,
|
||||
useCommandPalette,
|
||||
} from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { useIssueTypes } from "@/plane-web/hooks/store";
|
||||
// images
|
||||
import { useFlag, useIssueTypes } from "@/plane-web/hooks/store";
|
||||
import emptyProject from "@/public/empty-state/onboarding/dashboard-light.webp";
|
||||
|
||||
interface IProjectAuthWrapper {
|
||||
@@ -46,7 +47,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
const { fetchProjectStates } = useProjectState();
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { getProjectEstimates } = useProjectEstimates();
|
||||
const { fetchAllPropertiesAndOptions } = useIssueTypes();
|
||||
const { isIssueTypeEnabledForProject, fetchAllPropertiesAndOptions } = useIssueTypes();
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
@@ -103,10 +104,11 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
||||
|
||||
const isIssueTypeEnabled =
|
||||
projectExists?.is_issue_type_enabled && useFlag(workspaceSlug?.toString(), "ISSUE_TYPE_DISPLAY");
|
||||
const isIssueTypeEnabled = isIssueTypeEnabledForProject(
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
"ISSUE_TYPE_DISPLAY"
|
||||
);
|
||||
// fetching all issue types and properties
|
||||
useSWR(
|
||||
workspaceSlug && projectId && isIssueTypeEnabled
|
||||
@@ -128,6 +130,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
||||
// check if the user don't have permission to access the project
|
||||
if (projectExists && projectId && hasPermissionToProject[projectId.toString()] === false) return <JoinProject />;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { ServiceAPITokenService } from "@/plane-web/services/api_token.service";
|
||||
import { Spinner } from "@plane/ui";
|
||||
|
||||
interface CustomIframeProps {
|
||||
srcBase: string;
|
||||
@@ -69,7 +70,7 @@ const SiloIframe: React.FC<CustomIframeProps> = ({ srcBase }) => {
|
||||
hidden: isLoaded,
|
||||
})}
|
||||
>
|
||||
<LogoSpinner />
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,14 @@ import React, { useRef, useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Popover } from "@headlessui/react";
|
||||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
|
||||
import { IssueTypeLogo } from "./issue-type-logo";
|
||||
// local components
|
||||
import { IssueTypeLogo, TIssueTypeLogoSize } from "./issue-type-logo";
|
||||
import { LucideIconsList } from "./lucide-icons-list";
|
||||
|
||||
export type TIssueTypeIconPicker = {
|
||||
@@ -19,8 +23,7 @@ export type TIssueTypeIconPicker = {
|
||||
icon_props: TLogoProps["icon"];
|
||||
onChange: (value: TLogoProps["icon"]) => void;
|
||||
placement?: Placement;
|
||||
size?: number;
|
||||
containerSize?: number;
|
||||
size?: TIssueTypeLogoSize;
|
||||
};
|
||||
|
||||
export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
@@ -35,7 +38,6 @@ export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
icon_props,
|
||||
onChange,
|
||||
size,
|
||||
containerSize,
|
||||
placement = "bottom-start",
|
||||
} = props;
|
||||
// refs
|
||||
@@ -69,12 +71,7 @@ export const IssueTypeIconPicker: React.FC<TIssueTypeIconPicker> = (props) => {
|
||||
disabled={disabled}
|
||||
onClick={() => handleToggle(!isOpen)}
|
||||
>
|
||||
<IssueTypeLogo
|
||||
icon_props={icon_props}
|
||||
size={size}
|
||||
containerSize={containerSize}
|
||||
containerClassName={iconContainerClassName}
|
||||
/>
|
||||
<IssueTypeLogo icon_props={icon_props} size={size} containerClassName={iconContainerClassName} />
|
||||
</button>
|
||||
</Popover.Button>
|
||||
{isOpen && (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { FC } from "react";
|
||||
import { OctagonAlert } from "lucide-react";
|
||||
// types
|
||||
import { TLogoProps } from "@plane/types";
|
||||
// ui
|
||||
@@ -7,16 +6,31 @@ import { LayersIcon, LUCIDE_ICONS_LIST } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export type TIssueTypeLogoSize = "sm" | "md" | "lg" | "xl";
|
||||
|
||||
type Props = {
|
||||
icon_props: TLogoProps["icon"];
|
||||
size?: number;
|
||||
containerSize?: number;
|
||||
size?: TIssueTypeLogoSize;
|
||||
containerClassName?: string;
|
||||
isDefault?: boolean;
|
||||
};
|
||||
|
||||
const iconSizeMap = {
|
||||
sm: 12.5,
|
||||
md: 14.5,
|
||||
lg: 18,
|
||||
xl: 25,
|
||||
};
|
||||
|
||||
const containerSizeMap = {
|
||||
sm: 18,
|
||||
md: 21,
|
||||
lg: 25.5,
|
||||
xl: 35.5,
|
||||
};
|
||||
|
||||
export const IssueTypeLogo: FC<Props> = (props) => {
|
||||
const { icon_props, size = 11, containerSize = 18, containerClassName, isDefault = false } = props;
|
||||
const { icon_props, size = "sm", containerClassName, isDefault = false } = props;
|
||||
// derived values
|
||||
const LucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === icon_props?.name);
|
||||
|
||||
@@ -27,16 +41,16 @@ export const IssueTypeLogo: FC<Props> = (props) => {
|
||||
<>
|
||||
<span
|
||||
style={{
|
||||
height: containerSize,
|
||||
width: containerSize,
|
||||
height: containerSizeMap[size],
|
||||
width: containerSizeMap[size],
|
||||
backgroundColor: icon_props?.background_color,
|
||||
}}
|
||||
className={cn("flex-shrink-0 grid place-items-center rounded bg-custom-background-80", containerClassName)} // fallback background color
|
||||
>
|
||||
{isDefault ? (
|
||||
<LayersIcon
|
||||
width={size}
|
||||
height={size}
|
||||
width={iconSizeMap[size]}
|
||||
height={iconSizeMap[size]}
|
||||
style={{
|
||||
color: icon_props?.color ?? "#ffffff", // fallback color
|
||||
}}
|
||||
@@ -45,8 +59,8 @@ export const IssueTypeLogo: FC<Props> = (props) => {
|
||||
LucideIcon && (
|
||||
<LucideIcon.element
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
height: iconSizeMap[size],
|
||||
width: iconSizeMap[size],
|
||||
color: icon_props?.color ?? "#ffffff", // fallback color
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,11 +30,11 @@ export const LucideIconsList: React.FC<TIconsListProps> = (props) => {
|
||||
<div className="flex flex-col sticky top-0 bg-custom-background-100">
|
||||
<div className="flex items-center px-2 py-[12px] w-full ">
|
||||
<div
|
||||
className={`relative flex items-center gap-2 bg-custom-background-90 h-10 rounded-lg w-full px-[30px] border ${isInputFocused ? "border-custom-primary-100" : "border-transparent"}`}
|
||||
className={`relative flex items-center gap-2 bg-custom-background-90 h-8 rounded-lg w-full px-[30px] border ${isInputFocused ? "border-custom-primary-100" : "border-transparent"}`}
|
||||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
>
|
||||
<Search className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
|
||||
<Search className="absolute left-2.5 bottom-2 h-3.5 w-3.5 text-custom-text-400" />
|
||||
<Input
|
||||
placeholder="Search"
|
||||
value={query}
|
||||
|
||||
@@ -60,8 +60,7 @@ export const CreateOrUpdateIssueTypeForm: React.FC<Props> = (props) => {
|
||||
icon: value,
|
||||
});
|
||||
}}
|
||||
size={28}
|
||||
containerSize={40}
|
||||
size="xl"
|
||||
/>
|
||||
<div className="space-y-1 flew-grow w-full">
|
||||
<Input
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user