Compare commits

..
Author SHA1 Message Date
Palanikannan1437 cdb07fc3c4 added css animations for saved state 2023-08-14 22:27:41 +05:30
Palanikannan1437 8b02f58498 added debounced updates for title and removed saved state after timout 2023-08-14 21:50:13 +05:30
Palanikannan1437 b6329da50f updated yarn lock 2023-08-14 20:56:45 +05:30
Palanikannan1437 cd2c632929 fixed gpt-assistant modal 2023-08-14 20:54:15 +05:30
Palanikannan1437 b590cb60fd matched theme colors 2023-08-14 20:53:57 +05:30
sriramveeraghanta b885f14dc4 type fixes 2023-08-14 19:43:28 +05:30
sriramveeraghanta e03c4487d1 type fixes 2023-08-14 19:40:01 +05:30
sriramveeraghanta b2e6dc3dbc type fixes 2023-08-14 19:39:34 +05:30
sriramveeraghanta 70955b48d8 build fixes 2023-08-14 19:10:24 +05:30
Aaryan Khandelwal b1c6eaf2f0 chore: update custom css variables 2023-08-14 18:51:54 +05:30
Aaryan Khandelwal 78bb3085f3 fix: slash command scroll posiiton 2023-08-14 18:30:46 +05:30
Aaryan Khandelwal bcc1131ec1 fix: merge conflicts 2023-08-14 14:57:52 +05:30
Palanikannan1437 6112a4ab31 removed extra gap at top and moved saved indicator to bottom 2023-08-11 10:45:22 +05:30
sriramveeraghanta e325dc50ee command pallet fixes and removing logs 2023-08-10 16:30:41 +05:30
sriramveeraghanta be5bc765fc bugfixes 2023-08-10 16:00:55 +05:30
sriramveeraghanta 675d21c3f0 Merge branch 'develop' of github.com:makeplane/plane into tip-tap-integration 2023-08-10 15:59:47 +05:30
sriramveeraghanta dfba092af5 merge conflict resolved 2023-08-10 13:53:03 +05:30
Palanikannan1437 124383ebb4 explicitly added prosemirror deps 2023-08-10 13:16:25 +05:30
Palanikannan1437 7b3862a3dd trying pnpm overrides 2023-08-10 12:17:53 +05:30
Palanikannan1437 c1d2b41a80 added resolutions to prosemirror-model version 2023-08-10 11:23:33 +05:30
Palanikannan1437 b86081d788 added min height for issue description in modal 2023-08-10 04:11:44 +05:30
Palanikannan1437 6c9b31a2c4 temp fix 2023-08-10 03:57:16 +05:30
Palanikannan1437 1c08ad506a temp fix 2023-08-10 03:50:59 +05:30
Palanikannan1437 2329abe7dd added ts fix for node types 2023-08-10 03:40:01 +05:30
Palanikannan1437 ee6a6ecf3f fixing node ts error for Horizontal Rule 2023-08-10 03:31:38 +05:30
Palanikannan1437 a96514dc37 fixing ts errors for next build 2023-08-10 03:20:20 +05:30
Palanikannan1437 78a77cf560 improved code structure for extracting props in Tiptap 2023-08-10 02:50:34 +05:30
Palanikannan1437 96ef0a1e4f removed remirror instances and cleaned up code 2023-08-10 01:28:40 +05:30
Palanikannan1437 579051db31 added tiptap support to gpt assistant modal (yet to be tested) 2023-08-10 01:22:07 +05:30
Palanikannan1437 c2a4cdfebb added tiptap support for plane pages 2023-08-10 01:21:36 +05:30
Palanikannan1437 95358503ed added ref control support and bubble menu support for readonly editor 2023-08-10 01:21:15 +05:30
Palanikannan1437 b1dc5f3da1 added tiptap integration with user activity feeds 2023-08-10 01:20:40 +05:30
Palanikannan1437 0b6d510cc7 added additional props and Tiptap Integration with Comments 2023-08-10 01:20:15 +05:30
Palanikannan1437 5c290e1302 Integrated tiptap with Issue Modal 2023-08-09 09:42:57 +05:30
Palanikannan1437 5228ab8d0a improved file structure and delete image function implemented 2023-08-09 01:57:08 +05:30
Palanikannan1437 c078d59916 Image uploads done 2023-08-08 16:20:19 +05:30
Palanikannan1437 63c7bc2d68 added migration support by updating to html 2023-08-07 21:02:39 +05:30
Palanikannan1437 a6ae849a81 saving with debounce logic added and it's stored in backend 2023-08-05 04:53:10 +05:30
Palanikannan1437 50e7c5924c fixed bubble menu to match styles and added better seperation in UI 2023-08-05 04:51:58 +05:30
Palanikannan1437 60f1b7346d added support for slash command 2023-08-05 04:50:59 +05:30
Palanikannan1437 727570e347 fixed css for task lists and code with syntax highlighting 2023-08-05 04:50:18 +05:30
Palanikannan1437 b078e24d82 added bubblemenu support with extensions 2023-08-03 00:15:02 +05:30
Palanikannan1437 d62ac268c6 styles migrated for remirror to tiptap transition 2023-08-02 23:04:55 +05:30
Palanikannan1437 dd5ff737d1 remirror instances commented out to avoid prosemirror conflicts 2023-08-02 23:00:47 +05:30
159 changed files with 2225 additions and 3416 deletions
-2
View File
@@ -21,8 +21,6 @@ NEXT_PUBLIC_TRACK_EVENTS=0
NEXT_PUBLIC_SLACK_CLIENT_ID=""
# For Telemetry, set it to "app.plane.so"
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# public boards deploy url
NEXT_PUBLIC_DEPLOY_URL=""
# Backend
# Debug value for api server use it as 0 for production use
+3 -10
View File
@@ -5,11 +5,9 @@ WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
RUN apk add tree
COPY . .
RUN turbo prune --scope=app --scope=plane-deploy --docker
CMD tree -I node_modules/
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
@@ -23,14 +21,14 @@ COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install
# # Build the project
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
@@ -98,16 +96,11 @@ RUN adduser --system --uid 1001 captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
COPY --from=installer /app/apps/space/next.config.js .
COPY --from=installer /app/apps/space/package.json .
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
ENV NEXT_TELEMETRY_DISABLED 1
# RUN rm /etc/nginx/conf.d/default.conf
-10
View File
@@ -61,16 +61,6 @@ chmod +x setup.sh
> If running in a cloud env replace localhost with public facing IP address of the VM
- Setup Tiptap Pro
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
```
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
```
- Run Docker compose up
```bash
-10
View File
@@ -14,11 +14,6 @@ from plane.db.models import Cycle, CycleIssue, CycleFavorite
class CycleWriteSerializer(BaseSerializer):
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
class Meta:
model = Cycle
fields = "__all__"
@@ -40,11 +35,6 @@ class CycleSerializer(BaseSerializer):
started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
def validate(self, data):
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
raise serializers.ValidationError("Start date cannot exceed end date")
return data
def get_assignees(self, obj):
members = [
@@ -40,11 +40,6 @@ class ModuleWriteSerializer(BaseSerializer):
"updated_at",
]
def validate(self, data):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
members = validated_data.pop("members_list", None)
@@ -88,7 +88,6 @@ class ProjectLiteSerializer(BaseSerializer):
"cover_image",
"icon_prop",
"emoji",
"description",
]
read_only_fields = fields
@@ -104,7 +103,6 @@ class ProjectDetailSerializer(BaseSerializer):
is_member = serializers.BooleanField(read_only=True)
sort_order = serializers.FloatField(read_only=True)
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
class Meta:
model = Project
-1
View File
@@ -48,7 +48,6 @@ class ExportIssuesEndpoint(BaseAPIView):
project_ids=project_ids,
token_id=exporter.token,
multiple=multiple,
slug=slug,
)
return Response(
{
+1 -15
View File
@@ -20,17 +20,6 @@ class SlackProjectSyncViewSet(BaseViewSet):
serializer_class = SlackProjectSyncSerializer
model = SlackProjectSync
def get_queryset(self):
return (
super()
.get_queryset()
.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
)
.filter(project__project_projectmember__member=self.request.user)
)
def create(self, request, slug, project_id, workspace_integration_id):
try:
serializer = SlackProjectSyncSerializer(data=request.data)
@@ -56,10 +45,7 @@ class SlackProjectSyncViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Slack is already enabled for the project"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
except WorkspaceIntegration.DoesNotExist:
return Response(
{"error": "Workspace Integration does not exist"},
+1 -1
View File
@@ -370,7 +370,7 @@ class UserWorkSpaceIssues(BaseAPIView):
)
)
.filter(**filters)
).distinct()
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
+4 -20
View File
@@ -122,9 +122,7 @@ class ProjectViewSet(BaseViewSet):
)
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False
)
total_members=ProjectMember.objects.filter(project_id=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -147,14 +145,6 @@ class ProjectViewSet(BaseViewSet):
member_id=self.request.user.id,
).values("role")
)
.annotate(
is_deployed=Exists(
ProjectDeployBoard.objects.filter(
project_id=OuterRef("pk"),
workspace__slug=self.kwargs.get("slug"),
)
)
)
.distinct()
)
@@ -226,9 +216,7 @@ class ProjectViewSet(BaseViewSet):
project_id=serializer.data["id"], member=request.user, role=20
)
if serializer.data["project_lead"] is not None and str(
serializer.data["project_lead"]
) != str(request.user.id):
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(request.user.id):
ProjectMember.objects.create(
project_id=serializer.data["id"],
member_id=serializer.data["project_lead"],
@@ -395,9 +383,7 @@ class InviteProjectEndpoint(BaseAPIView):
validate_email(email)
# Check if user is already a member of workspace
if ProjectMember.objects.filter(
project_id=project_id,
member__email=email,
member__is_bot=False,
project_id=project_id, member__email=email
).exists():
return Response(
{"error": "User is already member of workspace"},
@@ -1101,9 +1087,7 @@ class ProjectMemberEndpoint(BaseAPIView):
def get(self, request, slug, project_id):
try:
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
member__is_bot=False,
project_id=project_id, workspace__slug=slug
).select_related("project", "member")
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
+7 -15
View File
@@ -47,7 +47,7 @@ from plane.api.serializers import (
WorkspaceThemeSerializer,
IssueActivitySerializer,
IssueLiteSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberAdminSerializer
)
from plane.api.views.base import BaseAPIView
from . import BaseViewSet
@@ -107,9 +107,7 @@ class WorkSpaceViewSet(BaseViewSet):
def get_queryset(self):
member_count = (
WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False
)
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -194,9 +192,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
def get(self, request):
try:
member_count = (
WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member__is_bot=False
)
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -629,9 +625,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
if (
workspace_member.role == 20
and WorkspaceMember.objects.filter(
workspace__slug=slug,
role=20,
member__is_bot=False,
workspace__slug=slug, role=20
).count()
== 1
):
@@ -994,11 +988,11 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
upcoming_issues = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]),
start_date__gte=timezone.now(),
target_date__gte=timezone.now(),
workspace__slug=slug,
assignees__in=[request.user],
completed_at__isnull=True,
).values("id", "name", "workspace__slug", "project_id", "start_date")
).values("id", "name", "workspace__slug", "project_id", "target_date")
return Response(
{
@@ -1083,7 +1077,6 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.filter(**filters)
.values("priority")
.annotate(priority_count=Count("priority"))
.filter(priority_count__gte=1)
.annotate(
priority_order=Case(
*[
@@ -1462,8 +1455,7 @@ class WorkspaceMembersEndpoint(BaseAPIView):
def get(self, request, slug):
try:
workspace_members = WorkspaceMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
workspace__slug=slug
).select_related("workspace", "member")
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
return Response(serialzier.data, status=status.HTTP_200_OK)
@@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug):
segmented = segment
assignee_details = {}
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
if x_axis in ["assignees__display_name"] or segment in ["assignees__display_name"]:
assignee_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
.order_by("assignees__id")
.distinct("assignees__id")
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name")
)
if segment:
@@ -93,19 +93,19 @@ def analytic_export_task(email, data, slug):
else:
generated_row.append("0")
# x-axis replacement for names
if x_axis in ["assignees__id"]:
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
if x_axis in ["assignees__display_name"]:
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
if len(assignee):
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows.append(tuple(generated_row))
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
if segmented in ["assignees__id"]:
if segmented in ["assignees__display_name"]:
for index, segm in enumerate(row_zero[2:]):
# find the name of the user
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)]
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(segm)]
if len(assignee):
row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
rows = [tuple(row_zero)] + rows
csv_buffer = io.StringIO()
@@ -141,8 +141,8 @@ def analytic_export_task(email, data, slug):
else distribution.get(item)[0].get("estimate "),
]
# x-axis replacement to names
if x_axis in ["assignees__id"]:
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
if x_axis in ["assignees__display_name"]:
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
if len(assignee):
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
+40 -29
View File
@@ -4,6 +4,7 @@ import io
import json
import boto3
import zipfile
from datetime import datetime, date, timedelta
# Django imports
from django.conf import settings
@@ -14,18 +15,19 @@ from celery import shared_task
from sentry_sdk import capture_exception
from botocore.client import Config
from openpyxl import Workbook
from openpyxl.styles import NamedStyle
from openpyxl.utils.datetime import to_excel
# Module imports
from plane.db.models import Issue, ExporterHistory
from plane.db.models import Issue, ExporterHistory, Project
def dateTimeConverter(time):
if time:
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
return super().default(obj)
def dateConverter(time):
if time:
return time.strftime("%a, %d %b %Y")
def create_csv_file(data):
csv_buffer = io.StringIO()
@@ -39,16 +41,25 @@ def create_csv_file(data):
def create_json_file(data):
return json.dumps(data)
return json.dumps(data, cls=DateTimeEncoder)
def create_xlsx_file(data):
workbook = Workbook()
sheet = workbook.active
no_timezone_style = NamedStyle(name="no_timezone_style")
no_timezone_style.number_format = "yyyy-mm-dd hh:mm:ss"
for row in data:
sheet.append(row)
for column_cells in sheet.columns:
for cell in column_cells:
if isinstance(cell.value, datetime):
cell.style = no_timezone_style
cell.value = to_excel(cell.value.replace(tzinfo=None))
xlsx_buffer = io.BytesIO()
workbook.save(xlsx_buffer)
xlsx_buffer.seek(0)
@@ -65,15 +76,15 @@ def create_zip_file(files):
return zip_buffer
def upload_to_s3(zip_file, workspace_id, token_id, slug):
def upload_to_s3(zip_file, workspace_id, token_id):
s3 = boto3.client(
"s3",
region_name=settings.AWS_REGION,
region_name="ap-south-1",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
file_name = f"{workspace_id}/issues-{datetime.now().date()}.zip"
s3.upload_fileobj(
zip_file,
@@ -117,15 +128,15 @@ def generate_table_row(issue):
else "",
issue["labels__name"],
issue["issue_cycle__cycle__name"],
dateConverter(issue["issue_cycle__cycle__start_date"]),
dateConverter(issue["issue_cycle__cycle__end_date"]),
issue["issue_cycle__cycle__start_date"],
issue["issue_cycle__cycle__end_date"],
issue["issue_module__module__name"],
dateConverter(issue["issue_module__module__start_date"]),
dateConverter(issue["issue_module__module__target_date"]),
dateTimeConverter(issue["created_at"]),
dateTimeConverter(issue["updated_at"]),
dateTimeConverter(issue["completed_at"]),
dateTimeConverter(issue["archived_at"]),
issue["issue_module__module__start_date"],
issue["issue_module__module__target_date"],
issue["created_at"],
issue["updated_at"],
issue["completed_at"],
issue["archived_at"],
]
@@ -145,15 +156,15 @@ def generate_json_row(issue):
else "",
"Labels": issue["labels__name"],
"Cycle Name": issue["issue_cycle__cycle__name"],
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
"Cycle Start Date": issue["issue_cycle__cycle__start_date"],
"Cycle End Date": issue["issue_cycle__cycle__end_date"],
"Module Name": issue["issue_module__module__name"],
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
"Module Target Date": dateConverter(issue["issue_module__module__target_date"]),
"Created At": dateTimeConverter(issue["created_at"]),
"Updated At": dateTimeConverter(issue["updated_at"]),
"Completed At": dateTimeConverter(issue["completed_at"]),
"Archived At": dateTimeConverter(issue["archived_at"]),
"Module Start Date": issue["issue_module__module__start_date"],
"Module Target Date": issue["issue_module__module__target_date"],
"Created At": issue["created_at"],
"Updated At": issue["updated_at"],
"Completed At": issue["completed_at"],
"Archived At": issue["archived_at"],
}
@@ -233,7 +244,7 @@ def generate_xlsx(header, project_id, issues, files):
@shared_task
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple):
try:
exporter_instance = ExporterHistory.objects.get(token=token_id)
exporter_instance.status = "processing"
@@ -331,7 +342,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
)
zip_buffer = create_zip_file(files)
upload_to_s3(zip_buffer, workspace_id, token_id, slug)
upload_to_s3(zip_buffer, workspace_id, token_id)
except Exception as e:
exporter_instance = ExporterHistory.objects.get(token=token_id)
+13 -18
View File
@@ -184,24 +184,19 @@ def track_description(
if current_instance.get("description_html") != requested_data.get(
"description_html"
):
last_activity = IssueActivity.objects.filter(issue_id=issue_id).order_by("-created_at").first()
if(last_activity is not None and last_activity.field == "description" and actor.id == last_activity.actor_id):
last_activity.created_at = timezone.now()
last_activity.save(update_fields=["created_at"])
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("description_html"),
new_value=requested_data.get("description_html"),
field="description",
project=project,
workspace=project.workspace,
comment=f"updated the description to {requested_data.get('description_html')}",
)
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=current_instance.get("description_html"),
new_value=requested_data.get("description_html"),
field="description",
project=project,
workspace=project.workspace,
comment=f"updated the description to {requested_data.get('description_html')}",
)
)
# Track changes in issue target date
@@ -0,0 +1,965 @@
# Generated by Django 4.2.3 on 2023-08-04 11:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.project
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='analyticview',
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.AlterField(
model_name='analyticview',
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.AlterField(
model_name='apitoken',
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.AlterField(
model_name='apitoken',
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.AlterField(
model_name='cycle',
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.AlterField(
model_name='cycle',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cycle',
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.AlterField(
model_name='cycle',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='cyclefavorite',
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.AlterField(
model_name='cyclefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cyclefavorite',
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.AlterField(
model_name='cyclefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='cycleissue',
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.AlterField(
model_name='cycleissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='cycleissue',
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.AlterField(
model_name='cycleissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='estimate',
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.AlterField(
model_name='estimate',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='estimate',
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.AlterField(
model_name='estimate',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='estimatepoint',
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.AlterField(
model_name='estimatepoint',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='estimatepoint',
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.AlterField(
model_name='estimatepoint',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='fileasset',
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.AlterField(
model_name='fileasset',
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.AlterField(
model_name='githubcommentsync',
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.AlterField(
model_name='githubcommentsync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubcommentsync',
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.AlterField(
model_name='githubcommentsync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubissuesync',
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.AlterField(
model_name='githubissuesync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubissuesync',
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.AlterField(
model_name='githubissuesync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubrepository',
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.AlterField(
model_name='githubrepository',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubrepository',
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.AlterField(
model_name='githubrepository',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='githubrepositorysync',
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.AlterField(
model_name='githubrepositorysync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='githubrepositorysync',
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.AlterField(
model_name='githubrepositorysync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='importer',
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.AlterField(
model_name='importer',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='importer',
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.AlterField(
model_name='importer',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='inbox',
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.AlterField(
model_name='inbox',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='inbox',
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.AlterField(
model_name='inbox',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='inboxissue',
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.AlterField(
model_name='inboxissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='inboxissue',
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.AlterField(
model_name='inboxissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='integration',
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.AlterField(
model_name='integration',
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.AlterField(
model_name='issue',
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.AlterField(
model_name='issue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issue',
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.AlterField(
model_name='issue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueactivity',
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.AlterField(
model_name='issueactivity',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueactivity',
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.AlterField(
model_name='issueactivity',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueassignee',
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.AlterField(
model_name='issueassignee',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueassignee',
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.AlterField(
model_name='issueassignee',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueattachment',
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.AlterField(
model_name='issueattachment',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueattachment',
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.AlterField(
model_name='issueattachment',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueblocker',
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.AlterField(
model_name='issueblocker',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueblocker',
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.AlterField(
model_name='issueblocker',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuecomment',
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.AlterField(
model_name='issuecomment',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuecomment',
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.AlterField(
model_name='issuecomment',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuelabel',
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.AlterField(
model_name='issuelabel',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuelabel',
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.AlterField(
model_name='issuelabel',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuelink',
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.AlterField(
model_name='issuelink',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuelink',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='issuelink',
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.AlterField(
model_name='issuelink',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueproperty',
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.AlterField(
model_name='issueproperty',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueproperty',
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.AlterField(
model_name='issueproperty',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issuesequence',
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.AlterField(
model_name='issuesequence',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issuesequence',
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.AlterField(
model_name='issuesequence',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueview',
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.AlterField(
model_name='issueview',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueview',
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.AlterField(
model_name='issueview',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='issueviewfavorite',
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.AlterField(
model_name='issueviewfavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='issueviewfavorite',
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.AlterField(
model_name='issueviewfavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='label',
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.AlterField(
model_name='label',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='label',
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.AlterField(
model_name='label',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='module',
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.AlterField(
model_name='module',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='module',
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.AlterField(
model_name='module',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulefavorite',
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.AlterField(
model_name='modulefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulefavorite',
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.AlterField(
model_name='modulefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='moduleissue',
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.AlterField(
model_name='moduleissue',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='moduleissue',
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.AlterField(
model_name='moduleissue',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulelink',
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.AlterField(
model_name='modulelink',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulelink',
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.AlterField(
model_name='modulelink',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='modulemember',
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.AlterField(
model_name='modulemember',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='modulemember',
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.AlterField(
model_name='modulemember',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='page',
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.AlterField(
model_name='page',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='page',
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.AlterField(
model_name='page',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pageblock',
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.AlterField(
model_name='pageblock',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pageblock',
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.AlterField(
model_name='pageblock',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pagefavorite',
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.AlterField(
model_name='pagefavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pagefavorite',
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.AlterField(
model_name='pagefavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='pagelabel',
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.AlterField(
model_name='pagelabel',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='pagelabel',
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.AlterField(
model_name='pagelabel',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='project',
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.AlterField(
model_name='project',
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.AlterField(
model_name='projectfavorite',
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.AlterField(
model_name='projectfavorite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectfavorite',
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.AlterField(
model_name='projectfavorite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='projectidentifier',
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.AlterField(
model_name='projectidentifier',
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.AlterField(
model_name='projectmember',
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.AlterField(
model_name='projectmember',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectmember',
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.AlterField(
model_name='projectmember',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='projectmemberinvite',
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.AlterField(
model_name='projectmemberinvite',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='projectmemberinvite',
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.AlterField(
model_name='projectmemberinvite',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='slackprojectsync',
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.AlterField(
model_name='slackprojectsync',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='slackprojectsync',
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.AlterField(
model_name='slackprojectsync',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='socialloginconnection',
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.AlterField(
model_name='socialloginconnection',
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.AlterField(
model_name='state',
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.AlterField(
model_name='state',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.AlterField(
model_name='state',
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.AlterField(
model_name='state',
name='workspace',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace'),
),
migrations.AlterField(
model_name='team',
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.AlterField(
model_name='team',
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.AlterField(
model_name='teammember',
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.AlterField(
model_name='teammember',
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.AlterField(
model_name='workspace',
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.AlterField(
model_name='workspace',
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.AlterField(
model_name='workspaceintegration',
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.AlterField(
model_name='workspaceintegration',
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.AlterField(
model_name='workspacemember',
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.AlterField(
model_name='workspacemember',
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.AlterField(
model_name='workspacememberinvite',
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.AlterField(
model_name='workspacememberinvite',
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.AlterField(
model_name='workspacetheme',
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.AlterField(
model_name='workspacetheme',
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.CreateModel(
name='ProjectDeployBoard',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
('comments', models.BooleanField(default=False)),
('reactions', 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')),
('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Project Deploy Board',
'verbose_name_plural': 'Project Deploy Boards',
'db_table': 'project_deploy_boards',
'ordering': ('-created_at',),
'unique_together': {('project', 'anchor')},
},
),
]
@@ -1,243 +0,0 @@
# Generated by Django 4.2.3 on 2023-08-14 07:12
from django.conf import settings
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.exporter
import plane.db.models.project
import uuid
import random
import string
def generate_display_name(apps, schema_editor):
UserModel = apps.get_model("db", "User")
updated_users = []
for obj in UserModel.objects.all():
obj.display_name = (
obj.email.split("@")[0]
if len(obj.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
updated_users.append(obj)
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
def rectify_field_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
for obj in Model.objects.filter(field="assignee"):
obj.field = "assignees"
updated_activity.append(obj)
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
def update_assignee_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
# Get all the users
User = apps.get_model("db", "User")
users = User.objects.values("id", "email", "display_name")
for obj in Model.objects.filter(field="assignees"):
if bool(obj.new_value) and not bool(obj.old_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.new_value
]
if assigned_user:
obj.new_value = assigned_user[0].get("display_name")
obj.new_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
if bool(obj.old_value) and not bool(obj.new_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.old_value
]
if assigned_user:
obj.old_value = assigned_user[0].get("display_name")
obj.old_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
updated_activity.append(obj)
Model.objects.bulk_update(
updated_activity,
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
batch_size=200,
)
def update_name_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
update_activity = []
for obj in Model.objects.filter(field="name"):
obj.comment = obj.comment.replace("start date", "name")
update_activity.append(obj)
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
def random_cycle_order(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycles = []
for obj in CycleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_cycles.append(obj)
CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100)
def random_module_order(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_modules = []
for obj in ModuleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_modules.append(obj)
ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100)
def update_user_issue_properties(apps, schema_editor):
IssuePropertyModel = apps.get_model("db", "IssueProperty")
updated_issue_properties = []
for obj in IssuePropertyModel.objects.all():
obj.properties["start_date"] = True
updated_issue_properties.append(obj)
IssuePropertyModel.objects.bulk_update(
updated_issue_properties, ["properties"], batch_size=100
)
def workspace_member_properties(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_members = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props["properties"]["start_date"] = True
obj.default_props["properties"]["start_date"] = True
updated_workspace_members.append(obj)
WorkspaceMemberModel.objects.bulk_update(
updated_workspace_members, ["view_props", "default_props"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name='issuecomment',
name='access',
field=models.CharField(choices=[('INTERNAL', 'INTERNAL'), ('EXTERNAL', 'EXTERNAL')], default='INTERNAL', max_length=100),
),
migrations.AddField(
model_name='module',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name='user',
name='display_name',
field=models.CharField(default='', max_length=255),
),
migrations.CreateModel(
name='ExporterHistory',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('project', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(default=uuid.uuid4), blank=True, null=True, size=None)),
('provider', models.CharField(choices=[('json', 'json'), ('csv', 'csv'), ('xlsx', 'xlsx')], max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)),
('reason', models.TextField(blank=True)),
('key', models.TextField(blank=True)),
('url', models.URLField(blank=True, max_length=800, null=True)),
('token', models.CharField(default=plane.db.models.exporter.generate_token, max_length=255, unique=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')),
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_exporters', to=settings.AUTH_USER_MODEL)),
('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_exporters', to='db.workspace')),
],
options={
'verbose_name': 'Exporter',
'verbose_name_plural': 'Exporters',
'db_table': 'exporters',
'ordering': ('-created_at',),
},
),
migrations.CreateModel(
name='ProjectDeployBoard',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
('comments', models.BooleanField(default=False)),
('reactions', models.BooleanField(default=False)),
('votes', models.BooleanField(default=False)),
('views', models.JSONField(default=plane.db.models.project.get_default_views)),
('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')),
('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Project Deploy Board',
'verbose_name_plural': 'Project Deploy Boards',
'db_table': 'project_deploy_boards',
'ordering': ('-created_at',),
'unique_together': {('project', 'anchor')},
},
),
migrations.CreateModel(
name='IssueVote',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('vote', models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')])),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to=settings.AUTH_USER_MODEL)),
('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')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Issue Vote',
'verbose_name_plural': 'Issue Votes',
'db_table': 'issue_votes',
'ordering': ('-created_at',),
'unique_together': {('issue', 'actor')},
},
),
migrations.AlterField(
model_name='modulelink',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.RunPython(generate_display_name),
migrations.RunPython(rectify_field_issue_activity),
migrations.RunPython(update_assignee_issue_activity),
migrations.RunPython(update_name_activity),
migrations.RunPython(random_cycle_order),
migrations.RunPython(random_module_order),
migrations.RunPython(update_user_issue_properties),
migrations.RunPython(workspace_member_properties),
]
@@ -0,0 +1,101 @@
# Generated by Django 4.2.3 on 2023-08-04 09:12
import string
import random
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def generate_display_name(apps, schema_editor):
UserModel = apps.get_model("db", "User")
updated_users = []
for obj in UserModel.objects.all():
obj.display_name = (
obj.email.split("@")[0]
if len(obj.email.split("@"))
else "".join(random.choice(string.ascii_letters) for _ in range(6))
)
updated_users.append(obj)
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
def rectify_field_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
for obj in Model.objects.filter(field="assignee"):
obj.field = "assignees"
updated_activity.append(obj)
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
def update_assignee_issue_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
updated_activity = []
# Get all the users
User = apps.get_model("db", "User")
users = User.objects.values("id", "email", "display_name")
for obj in Model.objects.filter(field="assignees"):
if bool(obj.new_value) and not bool(obj.old_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.new_value
]
if assigned_user:
obj.new_value = assigned_user[0].get("display_name")
obj.new_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
if bool(obj.old_value) and not bool(obj.new_value):
# Get user from list
assigned_user = [
user for user in users if user.get("email") == obj.old_value
]
if assigned_user:
obj.old_value = assigned_user[0].get("display_name")
obj.old_identifier = assigned_user[0].get("id")
# Update the comment
words = obj.comment.split()
words[-1] = assigned_user[0].get("display_name")
obj.comment = " ".join(words)
updated_activity.append(obj)
Model.objects.bulk_update(
updated_activity,
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
batch_size=200,
)
def update_name_activity(apps, schema_editor):
Model = apps.get_model("db", "IssueActivity")
update_activity = []
for obj in Model.objects.filter(field="name"):
obj.comment = obj.comment.replace("start date", "name")
update_activity.append(obj)
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
class Migration(migrations.Migration):
dependencies = [
("db", "0040_projectmember_preferences_user_cover_image_and_more"),
]
operations = [
migrations.AddField(
model_name="user",
name="display_name",
field=models.CharField(default="", max_length=255),
),
migrations.RunPython(generate_display_name),
migrations.RunPython(rectify_field_issue_activity),
migrations.RunPython(update_assignee_issue_activity),
migrations.RunPython(update_name_activity),
]
@@ -0,0 +1,30 @@
# Generated by Django 4.2.3 on 2023-08-09 12:15
import random
from django.db import migrations
def random_cycle_order(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycles = []
for obj in CycleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_cycles.append(obj)
CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100)
def random_module_order(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_modules = []
for obj in ModuleModel.objects.all():
obj.sort_order = random.randint(1, 65536)
updated_modules.append(obj)
ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0041_user_display_name_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.RunPython(random_cycle_order),
migrations.RunPython(random_module_order),
]
@@ -0,0 +1,38 @@
# Generated by Django 4.2.3 on 2023-08-09 11:15
from django.db import migrations
def update_user_issue_properties(apps, schema_editor):
IssuePropertyModel = apps.get_model("db", "IssueProperty")
updated_issue_properties = []
for obj in IssuePropertyModel.objects.all():
obj.properties["start_date"] = True
updated_issue_properties.append(obj)
IssuePropertyModel.objects.bulk_update(
updated_issue_properties, ["properties"], batch_size=100
)
def workspace_member_properties(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_members = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props["properties"]["start_date"] = True
obj.default_props["properties"]["start_date"] = True
updated_workspace_members.append(obj)
WorkspaceMemberModel.objects.bulk_update(
updated_workspace_members, ["view_props", "default_props"], batch_size=100
)
class Migration(migrations.Migration):
dependencies = [
("db", "0042_alter_analyticview_created_by_and_more"),
]
operations = [
migrations.RunPython(update_user_issue_properties),
migrations.RunPython(workspace_member_properties),
]
+1 -1
View File
@@ -98,7 +98,7 @@ class ModuleIssue(ProjectBaseModel):
class ModuleLink(ProjectBaseModel):
title = models.CharField(max_length=255, blank=True, null=True)
title = models.CharField(max_length=255, null=True)
url = models.URLField()
module = models.ForeignKey(
Module, on_delete=models.CASCADE, related_name="link_module"
+2 -2
View File
@@ -33,8 +33,8 @@ RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
FROM node:18-alpine AS runner
WORKDIR /app
@@ -1,8 +1,3 @@
// ui
import { ProfileEmptyState } from "components/ui";
// image
import emptyUsers from "public/empty-state/empty_users.svg";
type Props = {
users: {
avatar: string | null;
@@ -13,16 +8,10 @@ type Props = {
id: string;
}[];
title: string;
emptyStateMessage: string;
workspaceSlug: string;
};
export const AnalyticsLeaderboard: React.FC<Props> = ({
users,
title,
emptyStateMessage,
workspaceSlug,
}) => (
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title, workspaceSlug }) => (
<div className="p-3 border border-custom-border-200 rounded-[10px]">
<h6 className="text-base font-medium">{title}</h6>
{users.length > 0 ? (
@@ -58,9 +47,7 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({
))}
</div>
) : (
<div className="px-7 py-4">
<ProfileEmptyState title="No Data yet" description={emptyStateMessage} image={emptyUsers} />
</div>
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)}
</div>
);
@@ -63,7 +63,6 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
id: user?.created_by__id,
}))}
title="Most issues created"
emptyStateMessage="Co-workers and the number issues created by them appears here."
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
<AnalyticsLeaderboard
@@ -76,7 +75,6 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
id: user?.assignees__id,
}))}
title="Most issues closed"
emptyStateMessage="Co-workers and the number issues closed by them appears here."
workspaceSlug={workspaceSlug?.toString() ?? ""}
/>
<div className={fullScreen ? "md:col-span-2" : ""}>
@@ -1,7 +1,5 @@
// ui
import { BarGraph, ProfileEmptyState } from "components/ui";
// image
import emptyBarGraph from "public/empty-state/empty_bar_graph.svg";
import { BarGraph } from "components/ui";
// types
import { IDefaultAnalyticsResponse } from "types";
@@ -72,12 +70,8 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
}}
/>
) : (
<div className="px-7 py-4">
<ProfileEmptyState
title="No Data yet"
description="Analysis of pending issues by co-workers appears here."
image={emptyBarGraph}
/>
<div className="text-custom-text-200 text-center text-sm py-8">
No matching data found.
</div>
)}
</div>
@@ -1,7 +1,5 @@
// ui
import { LineGraph, ProfileEmptyState } from "components/ui";
// image
import emptyGraph from "public/empty-state/empty_graph.svg";
import { LineGraph } from "components/ui";
// types
import { IDefaultAnalyticsResponse } from "types";
// constants
@@ -50,13 +48,7 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
enableArea
/>
) : (
<div className="px-7 py-4">
<ProfileEmptyState
title="No Data yet"
description="Close issues to view analysis of the same in the form of a graph."
image={emptyGraph}
/>
</div>
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
)}
</div>
);
@@ -9,18 +9,12 @@ import userService from "services/user.service";
import useUser from "hooks/use-user";
// helper
import { unsetCustomCssVariables } from "helpers/theme.helper";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
};
export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpen }) => {
const store: any = useMobxStore();
export const ChangeInterfaceTheme: React.FC<Props> = ({ setIsPaletteOpen }) => {
const [mounted, setMounted] = useState(false);
const { setTheme } = useTheme();
@@ -29,11 +23,29 @@ export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpe
const updateUserTheme = (newTheme: string) => {
if (!user) return;
unsetCustomCssVariables();
setTheme(newTheme);
return store.user
.updateCurrentUserSettings({ theme: { ...user.theme, theme: newTheme } })
.then((response: any) => response)
.catch((error: any) => error);
mutateUser((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
theme: {
...prevData?.theme,
theme: newTheme,
},
};
}, false);
userService.updateUser({
theme: {
...user.theme,
theme: newTheme,
},
});
};
// useEffect only runs on the client, so now we can safely show the UI
@@ -62,4 +74,4 @@ export const ChangeInterfaceTheme: React.FC<Props> = observer(({ setIsPaletteOpe
))}
</>
);
});
};
@@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC<Props> = ({
return (
<div
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${isOpen ? "block" : "hidden"
}`}
className={`absolute ${inset} z-20 w-full space-y-4 rounded-[10px] border border-custom-border-200 bg-custom-background-100 p-4 shadow ${
isOpen ? "block" : "hidden"
}`}
>
{((content && content !== "") || (htmlContent && htmlContent !== "<p></p>")) && (
<div className="text-sm">
<div id="tiptap-container" className="text-sm">
Content:
<TiptapEditor
workspaceSlug={workspaceSlug as string}
value={htmlContent ?? `<p>${content}</p>`}
customClassName="-m-3"
noBorder
@@ -161,7 +161,6 @@ export const GptAssistantModal: React.FC<Props> = ({
<div className="page-block-section text-sm">
Response:
<Tiptap
workspaceSlug={workspaceSlug as string}
value={`<p>${response}</p>`}
customClassName="-mx-3 -my-3"
noBorder
@@ -180,10 +179,11 @@ export const GptAssistantModal: React.FC<Props> = ({
type="text"
name="task"
register={register}
placeholder={`${content && content !== ""
placeholder={`${
content && content !== ""
? "Tell AI what action to perform on this content..."
: "Ask AI anything..."
}`}
}`}
autoComplete="off"
/>
<div className={`flex gap-2 ${response === "" ? "justify-end" : "justify-between"}`}>
@@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC<Props> = ({
{isSubmitting
? "Generating response..."
: response === ""
? "Generate response"
: "Generate again"}
? "Generate response"
: "Generate again"}
</PrimaryButton>
</div>
</div>
+12 -44
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
// react-hook-form
import { useForm } from "react-hook-form";
@@ -7,15 +7,12 @@ import { Dialog, Transition } from "@headlessui/react";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IIssueLink, linkDetails, ModuleLink } from "types";
import type { IIssueLink, ModuleLink } from "types";
type Props = {
isOpen: boolean;
handleClose: () => void;
data?: linkDetails | null;
status: boolean;
createIssueLink: (formData: IIssueLink | ModuleLink) => Promise<void>;
updateIssueLink: (formData: IIssueLink | ModuleLink, linkId: string) => Promise<void>;
onFormSubmit: (formData: IIssueLink | ModuleLink) => Promise<void>;
};
const defaultValues: IIssueLink | ModuleLink = {
@@ -23,14 +20,7 @@ const defaultValues: IIssueLink | ModuleLink = {
url: "",
};
export const LinkModal: React.FC<Props> = ({
isOpen,
handleClose,
createIssueLink,
updateIssueLink,
status,
data,
}) => {
export const LinkModal: React.FC<Props> = ({ isOpen, handleClose, onFormSubmit }) => {
const {
register,
formState: { errors, isSubmitting },
@@ -40,6 +30,11 @@ export const LinkModal: React.FC<Props> = ({
defaultValues,
});
const onSubmit = async (formData: IIssueLink | ModuleLink) => {
await onFormSubmit({ title: formData.title, url: formData.url });
onClose();
};
const onClose = () => {
handleClose();
const timeout = setTimeout(() => {
@@ -48,27 +43,6 @@ export const LinkModal: React.FC<Props> = ({
}, 500);
};
const handleFormSubmit = async (formData: IIssueLink | ModuleLink) => {
if (!data) await createIssueLink({ title: formData.title, url: formData.url });
else await updateIssueLink({ title: formData.title, url: formData.url }, data.id);
onClose();
};
const handleCreateUpdatePage = async (formData: IIssueLink | ModuleLink) => {
await handleFormSubmit(formData);
reset({
...defaultValues,
});
};
useEffect(() => {
reset({
...defaultValues,
...data,
});
}, [data, reset]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
@@ -96,14 +70,14 @@ export const LinkModal: React.FC<Props> = ({
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-200 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div className="space-y-5">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
{status ? "Update Link" : "Add Link"}
Add Link
</Dialog.Title>
<div className="mt-2 space-y-3">
<div>
@@ -139,13 +113,7 @@ export const LinkModal: React.FC<Props> = ({
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{status
? isSubmitting
? "Updating Link..."
: "Update Link"
: isSubmitting
? "Adding Link..."
: "Add Link"}
{isSubmitting ? "Adding Link..." : "Add Link"}
</PrimaryButton>
</div>
</form>
+11 -17
View File
@@ -1,24 +1,25 @@
// icons
import { ArrowTopRightOnSquareIcon, LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
import { Icon } from "components/ui";
// helpers
import { timeAgo } from "helpers/date-time.helper";
// types
import { linkDetails, UserAuth } from "types";
import { IUserLite, UserAuth } from "types";
type Props = {
links: linkDetails[];
links: {
id: string;
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
metadata: any;
title: string;
url: string;
}[];
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: linkDetails) => void;
userAuth: UserAuth;
};
export const LinksList: React.FC<Props> = ({
links,
handleDeleteLink,
handleEditLink,
userAuth,
}) => {
export const LinksList: React.FC<Props> = ({ links, handleDeleteLink, userAuth }) => {
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return (
@@ -27,13 +28,6 @@ export const LinksList: React.FC<Props> = ({
<div key={link.id} className="relative">
{!isNotAllowed && (
<div className="absolute top-1.5 right-1.5 z-[1] flex items-center gap-1">
<button
type="button"
className="grid h-7 w-7 place-items-center rounded bg-custom-background-90 p-1 outline-none hover:bg-custom-background-80"
onClick={() => handleEditLink(link)}
>
<Icon iconName="edit" className="text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
@@ -86,10 +86,8 @@ export const SingleBoardIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const [isMenuActive, setIsMenuActive] = useState(false);
const [isDropdownActive, setIsDropdownActive] = useState(false);
const actionSectionRef = useRef<HTMLDivElement | null>(null);
@@ -202,7 +200,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
clickEvent={contextMenuPosition}
position={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -244,10 +242,10 @@ export const SingleBoardIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition(e);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<div className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px]">
<div className="group/card relative select-none p-3.5">
{!isNotAllowed && (
<div
ref={actionSectionRef}
@@ -297,20 +295,16 @@ export const SingleBoardIssue: React.FC<Props> = ({
</div>
)}
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
<a className="flex flex-col gap-1.5">
<a>
{properties.key && (
<div className="text-xs font-medium text-custom-text-200">
<div className="mb-2.5 text-xs font-medium text-custom-text-200">
{issue.project_detail.identifier}-{issue.sequence_id}
</div>
)}
<h5 className="text-sm break-words line-clamp-2">{issue.name}</h5>
</a>
</Link>
<div
className={`flex items-center gap-2 text-xs ${
isDropdownActive ? "" : "overflow-x-scroll"
}`}
>
<div className="mt-2.5 flex overflow-x-scroll items-center gap-2 text-xs">
{properties.priority && (
<ViewPrioritySelect
issue={issue}
@@ -333,8 +327,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
<ViewStartDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
handleOnOpen={() => setIsDropdownActive(true)}
handleOnClose={() => setIsDropdownActive(false)}
user={user}
isNotAllowed={isNotAllowed}
/>
@@ -343,8 +335,6 @@ export const SingleBoardIssue: React.FC<Props> = ({
<ViewDueDateSelect
issue={issue}
partialUpdateIssue={partialUpdateIssue}
handleOnOpen={() => setIsDropdownActive(true)}
handleOnClose={() => setIsDropdownActive(false)}
user={user}
isNotAllowed={isNotAllowed}
/>
@@ -33,7 +33,7 @@ import {
} from "@heroicons/react/24/outline";
import { LayerDiagonalIcon } from "components/icons";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue";
// types
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
@@ -71,7 +71,7 @@ export const SingleListIssue: React.FC<Props> = ({
}) => {
// context menu
const [contextMenu, setContextMenu] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -167,7 +167,7 @@ export const SingleListIssue: React.FC<Props> = ({
return (
<>
<ContextMenu
clickEvent={contextMenuPosition}
position={contextMenuPosition}
title="Quick actions"
isOpen={contextMenu}
setIsOpen={setContextMenu}
@@ -199,7 +199,7 @@ export const SingleListIssue: React.FC<Props> = ({
onContextMenu={(e) => {
e.preventDefault();
setContextMenu(true);
setContextMenuPosition(e);
setContextMenuPosition({ x: e.pageX, y: e.pageY });
}}
>
<div className="flex-grow cursor-pointer min-w-[200px] whitespace-nowrap overflow-hidden overflow-ellipsis">
@@ -45,6 +45,7 @@ type TSingleStatProps = {
handleDeleteCycle: () => void;
handleAddToFavorites: () => void;
handleRemoveFromFavorites: () => void;
isCompleted?: boolean;
};
const stateGroups = [
@@ -81,6 +82,7 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
handleDeleteCycle,
handleAddToFavorites,
handleRemoveFromFavorites,
isCompleted = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -88,7 +90,6 @@ export const SingleCycleCard: React.FC<TSingleStatProps> = ({
const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
@@ -34,6 +34,7 @@ type TSingleStatProps = {
handleDeleteCycle: () => void;
handleAddToFavorites: () => void;
handleRemoveFromFavorites: () => void;
isCompleted?: boolean;
};
const stateGroups = [
@@ -112,6 +113,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
handleDeleteCycle,
handleAddToFavorites,
handleRemoveFromFavorites,
isCompleted = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -119,7 +121,6 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
const { setToastAlert } = useToast();
const cycleStatus = getDateRangeStatus(cycle.start_date, cycle.end_date);
const isCompleted = cycleStatus === "completed";
const endDate = new Date(cycle.end_date ?? "");
const startDate = new Date(cycle.start_date ?? "");
+1 -1
View File
@@ -68,7 +68,7 @@ const IntegrationGuide = () => {
<p className="text-sm text-custom-text-200">{service.description}</p>
</div>
<div className="flex-shrink-0">
<Link href={`/${workspaceSlug}/settings/exports?provider=${service.provider}`}>
<Link href={`/${workspaceSlug}/settings/export?provider=${service.provider}`}>
<a>
<PrimaryButton>
<span className="capitalize">{service.type}</span> now
@@ -293,7 +293,6 @@ export const InboxMainContent: React.FC = () => {
</div>
<div>
<IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={{
name: issueDetails.name,
description: issueDetails.description,
+1 -1
View File
@@ -100,7 +100,7 @@ const IntegrationGuide = () => {
</div>
<div className="flex-shrink-0">
<Link
href={`/${workspaceSlug}/settings/imports?provider=${service.provider}`}
href={`/${workspaceSlug}/settings/import?provider=${service.provider}`}
>
<a>
<PrimaryButton>
+2 -7
View File
@@ -134,9 +134,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
>
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name.charAt(0)
: activityItem.actor_detail.display_name.charAt(0)}
{activityItem.actor_detail.display_name.charAt(0)}
</div>
)}
</div>
@@ -155,9 +153,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
) : (
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
<a className="text-gray font-medium">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name
: activityItem.actor_detail.display_name}
{activityItem.actor_detail.display_name}
</a>
</Link>
)}{" "}
@@ -175,7 +171,6 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
return (
<div key={activityItem.id} className="mt-4">
<CommentCard
workspaceSlug={workspaceSlug as string}
comment={activityItem as IIssueComment}
onSubmit={handleCommentUpdate}
handleCommentDeletion={handleCommentDelete}
@@ -87,22 +87,21 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="issue-comments-section">
<div id="tiptap-container" className="issue-comments-section">
<Controller
name="comment_html"
control={control}
render={({ field: { value, onChange } }) => (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={
!value ||
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
value === "" ||
(typeof value === "object" && Object.keys(value).length === 0)
? watch("comment_html")
: value
}
customClassName="p-3 min-h-[50px] shadow-sm"
customClassName="p-3 min-h-[50px]"
debouncedUpdatesEnabled={false}
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
@@ -22,13 +22,12 @@ const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEdit
TiptapEditor.displayName = "TiptapEditor";
type Props = {
workspaceSlug: string;
comment: IIssueComment;
onSubmit: (comment: IIssueComment) => void;
handleCommentDeletion: (comment: string) => void;
};
export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => {
export const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion }) => {
const { user } = useUser();
const editorRef = React.useRef<any>(null);
@@ -66,11 +65,7 @@ export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit,
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
<img
src={comment.actor_detail.avatar}
alt={
comment.actor_detail.is_bot
? comment.actor_detail.first_name + " Bot"
: comment.actor_detail.display_name
}
alt={comment.actor_detail.display_name}
height={30}
width={30}
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
@@ -79,9 +74,7 @@ export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit,
<div
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}
>
{comment.actor_detail.is_bot
? comment.actor_detail.first_name.charAt(0)
: comment.actor_detail.display_name.charAt(0)}
{comment.actor_detail.display_name.charAt(0)}
</div>
)}
@@ -108,13 +101,12 @@ export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit,
className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}
onSubmit={handleSubmit(onEnter)}
>
<div>
<div id="tiptap-container">
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={watch("comment_html")}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
customClassName="min-h-[50px] p-3"
onChange={(comment_json: Object, comment_html: string) => {
setValue("comment_json", comment_json);
setValue("comment_html", comment_html);
@@ -140,7 +132,6 @@ export const CommentCard: React.FC<Props> = ({ comment, workspaceSlug, onSubmit,
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
value={comment.comment_html}
editable={false}
@@ -24,7 +24,6 @@ export interface IssueDetailsProps {
description: string;
description_html: string;
};
workspaceSlug: string;
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
isAllowed: boolean;
}
@@ -32,7 +31,6 @@ export interface IssueDetailsProps {
export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
issue,
handleFormSubmit,
workspaceSlug,
isAllowed,
}) => {
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
@@ -71,15 +69,11 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert]);
}, [isSubmitting]);
// reset form values
useEffect(() => {
@@ -128,7 +122,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
)}
</div>
<span>{errors.name ? errors.name.message : null}</span>
<div className="relative">
<div id="tiptap-container" className="relative">
<Controller
name="description_html"
control={control}
@@ -144,14 +138,11 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
? watch("description_html")
: value
}
workspaceSlug={workspaceSlug}
debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting}
customClassName="min-h-[150px] shadow-sm"
customClassName="min-h-[150px]"
editorContentCustomClassNames="pb-9"
onChange={(description: Object, description_html: string) => {
setShowAlert(true);
setIsSubmitting("submitting");
onChange(description_html);
setValue("description", description);
@@ -163,11 +154,8 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
);
}}
/>
<div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`}
>
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
<div className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === 'saved' ? 'fadeOut' : 'fadeIn'}`}>
{isSubmitting === 'submitting' ? 'Saving...' : 'Saved'}
</div>
</div>
</div>
+1 -2
View File
@@ -333,7 +333,7 @@ export const IssueForm: FC<IssueFormProps> = ({
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && (
<div className="relative">
<div id="tiptap-container" className="relative">
<div className="flex justify-end">
{issueName && issueName !== "" && (
<button
@@ -370,7 +370,6 @@ export const IssueForm: FC<IssueFormProps> = ({
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
debouncedUpdatesEnabled={false}
value={
@@ -124,7 +124,6 @@ export const IssueMainContent: React.FC<Props> = ({
</div>
) : null}
<IssueDescriptionForm
workspaceSlug={workspaceSlug as string}
issue={issueDetails}
handleFormSubmit={submitChanges}
isAllowed={memberRole.isMember || memberRole.isOwner || !uneditable}
@@ -270,18 +270,16 @@ export const MyIssuesView: React.FC<Props> = ({
? "You have not created any issue yet."
: "You have not subscribed to any issue yet.",
description: "Keep track of your work in a single place.",
primaryButton: filters.subscriber
? undefined
: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
primaryButton: {
icon: <PlusIcon className="h-4 w-4" />,
text: "New Issue",
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
},
}}
handleOnDragEnd={handleOnDragEnd}
handleIssueAction={handleIssueAction}
@@ -5,7 +5,9 @@ import useSWR from "swr";
// services
import projectServices from "services/project.service";
// ui
import { AssigneesList, Avatar, CustomSearchSelect, Icon } from "components/ui";
import { AssigneesList, Avatar, CustomSearchSelect } from "components/ui";
// icons
import { UserGroupIcon } from "@heroicons/react/24/outline";
// fetch-keys
import { PROJECT_MEMBERS } from "constants/fetch-keys";
@@ -42,15 +44,15 @@ export const IssueAssigneeSelect: React.FC<Props> = ({ projectId, value = [], on
value={value}
onChange={onChange}
options={options}
customButton={
<div className="flex items-center gap-2 cursor-pointer text-xs text-custom-text-200">
label={
<div className="flex items-center gap-2 text-custom-text-200">
{value && value.length > 0 && Array.isArray(value) ? (
<div className="-my-0.5 flex items-center justify-center gap-2">
<div className="flex items-center justify-center gap-2">
<AssigneesList userIds={value} length={3} showLength={true} />
</div>
) : (
<div className="flex items-center justify-center gap-2 px-1.5 py-1 rounded shadow-sm border border-custom-border-300 hover:bg-custom-background-80">
<Icon iconName="person" className="!text-base !leading-4" />
<div className="flex items-center justify-center gap-2">
<UserGroupIcon className="h-4 w-4 text-custom-text-200" />
<span className="text-custom-text-200">Assignee</span>
</div>
)}
+2 -4
View File
@@ -17,10 +17,10 @@ type Props = {
export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onChange, value }) => (
<Popover className="relative flex items-center justify-center rounded-lg">
{({ close }) => (
{({ open }) => (
<>
<Popover.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80">
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs text-custom-text-200">
{value ? (
<>
<span className="text-custom-text-100">{renderShortDateWithYearFormat(value)}</span>
@@ -52,8 +52,6 @@ export const IssueDateSelect: React.FC<Props> = ({ label, maxDate, minDate, onCh
onChange={(val) => {
if (!val) onChange("");
else onChange(renderDateFormat(val));
close();
}}
dateFormat="dd-MM-yyyy"
minDate={minDate}
+4 -4
View File
@@ -59,17 +59,17 @@ export const IssueLabelSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
>
{({ open }: any) => (
<>
<Combobox.Button className="flex items-center gap-2 cursor-pointer text-xs text-custom-text-200">
<Combobox.Button className="flex cursor-pointer items-center rounded-md border border-custom-border-200 text-xs shadow-sm duration-200 hover:bg-custom-background-80">
{value && value.length > 0 ? (
<span className="flex items-center justify-center gap-2 text-xs">
<span className="flex items-center justify-center gap-2 px-3 py-1 text-xs">
<IssueLabelsList
labels={value.map((v) => issueLabels?.find((l) => l.id === v)) ?? []}
labels={value.map((v) => issueLabels?.find((l) => l.id === v)?.color) ?? []}
length={3}
showLength={true}
/>
</span>
) : (
<span className="flex items-center justify-center gap-2 px-2 py-1 text-xs rounded shadow-sm border border-custom-border-300 hover:bg-custom-background-80">
<span className="flex items-center justify-center gap-2 px-2.5 py-1 text-xs">
<TagIcon className="h-3.5 w-3.5 text-custom-text-200" />
<span className=" text-custom-text-200">Label</span>
</span>
@@ -17,7 +17,13 @@ import useUser from "hooks/use-user";
// ui
import { Input, Spinner } from "components/ui";
// icons
import { PlusIcon, RectangleGroupIcon, TagIcon, XMarkIcon } from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
PlusIcon,
RectangleGroupIcon,
TagIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import { IIssue, IIssueLabels } from "types";
// fetch-keys
@@ -275,15 +281,18 @@ export const SidebarLabelSelect: React.FC<Props> = ({
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button className="grid place-items-center outline-none">
<Popover.Button
className={`flex items-center gap-1 rounded-md bg-custom-background-80 p-1 outline-none focus:ring-2 focus:ring-custom-primary`}
>
{watch("color") && watch("color") !== "" && (
<span
className="h-6 w-6 rounded"
className="h-5 w-5 rounded"
style={{
backgroundColor: watch("color") ?? "black",
}}
/>
)}
<ChevronDownIcon className="h-3 w-3" />
</Popover.Button>
<Transition
@@ -295,7 +304,7 @@ export const SidebarLabelSelect: React.FC<Props> = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
<Popover.Panel className="absolute right-0 bottom-8 z-10 mt-1 max-w-xs transform px-2 sm:px-0">
<Controller
name="color"
control={labelControl}
+7 -59
View File
@@ -37,7 +37,7 @@ import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/reac
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import type { ICycle, IIssue, IIssueLink, linkDetails, IModule } from "types";
import type { ICycle, IIssue, IIssueLink, IModule } from "types";
// fetch-keys
import { ISSUE_DETAILS } from "constants/fetch-keys";
@@ -77,7 +77,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
}) => {
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, issueId } = router.query;
@@ -157,43 +156,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
});
};
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
if (!workspaceSlug || !projectId || !issueDetail) return;
const payload = { metadata: {}, ...formData };
const updatedLinks = issueDetail.issue_link.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IIssue>(
ISSUE_DETAILS(issueDetail.id),
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
false
);
await issuesService
.updateIssueLink(
workspaceSlug as string,
projectId as string,
issueDetail.id,
linkId,
payload
)
.then((res) => {
mutate(ISSUE_DETAILS(issueDetail.id));
})
.catch((err) => {
console.log(err);
});
};
const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !issueDetail) return;
@@ -258,25 +220,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link);
setLinkModal(true);
};
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
return (
<>
<LinkModal
isOpen={linkModal}
handleClose={() => {
setLinkModal(false);
setSelectedLinkToUpdate(null);
}}
data={selectedLinkToUpdate}
status={selectedLinkToUpdate ? true : false}
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
handleClose={() => setLinkModal(false)}
onFormSubmit={handleCreateLink}
/>
<DeleteIssueModal
handleClose={() => setDeleteIssueModal(false)}
@@ -285,7 +236,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
user={user}
/>
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
<div className="flex items-center justify-between px-5 pb-3">
<div className="flex items-center justify-between pb-3">
<h4 className="text-sm font-medium">
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
</h4>
@@ -327,7 +278,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
</div>
</div>
<div className="h-full w-full px-5 overflow-y-auto">
<div className="h-full w-full overflow-y-auto">
<div className={`divide-y-2 divide-custom-border-200 ${uneditable ? "opacity-60" : ""}`}>
{showFirstSection && (
<div className="py-1">
@@ -445,8 +396,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
start_date: val,
})
}
className="bg-custom-background-100"
wrapperClassName="w-full"
className="bg-custom-background-90"
maxDate={maxDate ?? undefined}
disabled={isNotAllowed || uneditable}
/>
@@ -474,8 +424,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
target_date: val,
})
}
className="bg-custom-background-100"
wrapperClassName="w-full"
className="bg-custom-background-90"
minDate={minDate ?? undefined}
disabled={isNotAllowed || uneditable}
/>
@@ -539,7 +488,6 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<LinksList
links={issueDetail.issue_link}
handleDeleteLink={handleDeleteLink}
handleEditLink={handleEditLink}
userAuth={memberRole}
/>
) : null}
@@ -13,8 +13,6 @@ import useIssuesView from "hooks/use-issues-view";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
noBorder?: boolean;
user: ICurrentUserResponse | undefined;
@@ -24,8 +22,6 @@ type Props = {
export const ViewDueDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
noBorder = false,
user,
@@ -84,8 +80,6 @@ export const ViewDueDateSelect: React.FC<Props> = ({
}`}
minDate={minDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
handleOnClose={handleOnClose}
disabled={isNotAllowed}
/>
</div>
@@ -13,8 +13,6 @@ import useIssuesView from "hooks/use-issues-view";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
tooltipPosition?: "top" | "bottom";
noBorder?: boolean;
user: ICurrentUserResponse | undefined;
@@ -24,8 +22,6 @@ type Props = {
export const ViewStartDateSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
handleOnOpen,
handleOnClose,
tooltipPosition = "top",
noBorder = false,
user,
@@ -76,8 +72,6 @@ export const ViewStartDateSelect: React.FC<Props> = ({
}`}
maxDate={maxDate ?? undefined}
noBorder={noBorder}
handleOnOpen={handleOnOpen}
handleOnClose={handleOnClose}
disabled={isNotAllowed}
/>
</div>
+34 -13
View File
@@ -1,11 +1,15 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// hooks
import useToast from "hooks/use-toast";
// components
import { ModuleLeadSelect, ModuleMembersSelect, ModuleStatusSelect } from "components/modules";
// ui
import { DateSelect, Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helper
import { isDateRangeValid } from "helpers/date-time.helper";
// types
import { IModule } from "types";
@@ -25,6 +29,8 @@ const defaultValues: Partial<IModule> = {
};
export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data }) => {
const [isDateValid, setIsDateValid] = useState(true);
const { setToastAlert } = useToast();
const {
register,
formState: { errors, isSubmitting },
@@ -51,15 +57,6 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
});
}, [data, reset]);
const startDate = watch("start_date");
const targetDate = watch("target_date");
const minDate = startDate ? new Date(startDate) : null;
minDate?.setDate(minDate.getDate());
const maxDate = targetDate ? new Date(targetDate) : null;
maxDate?.setDate(maxDate.getDate());
return (
<form onSubmit={handleSubmit(handleCreateUpdateModule)}>
<div className="space-y-5">
@@ -106,8 +103,20 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
value={value}
onChange={(val) => {
onChange(val);
if (val && watch("target_date")) {
if (isDateRangeValid(val, `${watch("target_date")}`)) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
}}
maxDate={maxDate ?? undefined}
/>
)}
/>
@@ -120,8 +129,20 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
value={value}
onChange={(val) => {
onChange(val);
if (watch("start_date") && val) {
if (isDateRangeValid(`${watch("start_date")}`, val)) {
setIsDateValid(true);
} else {
setIsDateValid(false);
setToastAlert({
type: "error",
title: "Error!",
message:
"The date you have entered is invalid. Please check and enter a valid date.",
});
}
}
}}
minDate={minDate ?? undefined}
/>
)}
/>
@@ -145,7 +166,7 @@ export const ModuleForm: React.FC<Props> = ({ handleFormSubmit, handleClose, sta
</div>
<div className="-mx-5 mt-5 flex justify-end gap-2 border-t border-custom-border-200 px-5 pt-5">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
<PrimaryButton type="submit" loading={isSubmitting || isDateValid ? false : true}>
{status
? isSubmitting
? "Updating Module..."
+4 -48
View File
@@ -37,7 +37,7 @@ import { LinkIcon } from "@heroicons/react/20/solid";
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter, copyTextToClipboard } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue, linkDetails, IModule, ModuleLink } from "types";
import { ICurrentUserResponse, IIssue, IModule, ModuleLink } from "types";
// fetch-keys
import { MODULE_DETAILS } from "constants/fetch-keys";
// constant
@@ -61,7 +61,6 @@ type Props = {
export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIssues, user }) => {
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
@@ -116,37 +115,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
});
};
const handleUpdateLink = async (formData: ModuleLink, linkId: string) => {
if (!workspaceSlug || !projectId || !module) return;
const payload = { metadata: {}, ...formData };
const updatedLinks = module.link_module.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IModule>(
MODULE_DETAILS(module.id),
(prevData) => ({ ...(prevData as IModule), link_module: updatedLinks }),
false
);
await modulesService
.updateModuleLink(workspaceSlug as string, projectId as string, module.id, linkId, payload)
.then((res) => {
mutate(MODULE_DETAILS(module.id));
})
.catch((err) => {
console.log(err);
});
};
const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !module) return;
@@ -202,23 +170,12 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
? Math.round((module.completed_issues / module.total_issues) * 100)
: null;
const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link);
setModuleLinkModal(true);
};
return (
<>
<LinkModal
isOpen={moduleLinkModal}
handleClose={() => {
setModuleLinkModal(false);
setSelectedLinkToUpdate(null);
}}
data={selectedLinkToUpdate}
status={selectedLinkToUpdate ? true : false}
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
handleClose={() => setModuleLinkModal(false)}
onFormSubmit={handleCreateLink}
/>
<DeleteModuleModal
isOpen={moduleDeleteModal}
@@ -587,7 +544,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
</Disclosure>
</div>
<div className="flex w-full flex-col border-t border-custom-border-200 px-6 pt-6 pb-10 text-xs">
<div className="flex w-full flex-col border-t border-custom-border-200 px-6 py-6 text-xs">
<div className="flex w-full items-center justify-between">
<h4 className="text-sm font-medium text-custom-text-200">Links</h4>
<button
@@ -601,7 +558,6 @@ export const ModuleDetailsSidebar: React.FC<Props> = ({ module, isOpen, moduleIs
{memberRole && module.link_module && module.link_module.length > 0 ? (
<LinksList
links={module.link_module}
handleEditLink={handleEditLink}
handleDeleteLink={handleDeleteLink}
userAuth={memberRole}
/>
@@ -53,9 +53,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
onClick={() => {
markNotificationReadStatus(notification.id);
router.push(
`/${workspaceSlug}/projects/${notification.project}/${
notification.data.issue_activity.field === "archived_at" ? "archived-issues" : "issues"
}/${notification.data.issue.id}`
`/${workspaceSlug}/projects/${notification.project}/issues/${notification.data.issue.id}`
);
}}
className={`group w-full flex items-center gap-4 p-3 pl-6 relative cursor-pointer ${
@@ -80,9 +78,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
) : (
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
<span className="text-custom-text-100 font-medium text-lg">
{notification.triggered_by_details.is_bot ? (
notification.triggered_by_details.first_name?.[0]?.toUpperCase()
) : notification.triggered_by_details.display_name?.[0] ? (
{notification.triggered_by_details.display_name?.[0] ? (
notification.triggered_by_details.display_name?.[0]?.toUpperCase()
) : (
<Icon iconName="person" className="h-6 w-6" />
@@ -93,11 +89,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
</div>
<div className="space-y-2.5 w-full overflow-hidden">
<div className="text-sm w-full break-words">
<span className="font-semibold">
{notification.triggered_by_details.is_bot
? notification.triggered_by_details.first_name
: notification.triggered_by_details.display_name}{" "}
</span>
<span className="font-semibold">{notification.triggered_by_details.display_name} </span>
{notification.data.issue_activity.field !== "comment" &&
notification.data.issue_activity.verb}{" "}
{notification.data.issue_activity.field === "comment"
@@ -231,9 +231,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
description:
!data.description || data.description === ""
? {
type: "doc",
content: [{ type: "paragraph" }],
}
type: "doc",
content: [{ type: "paragraph" }],
}
: data.description,
description_html: data.description_html ?? "<p></p>",
});
@@ -284,7 +284,10 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
maxLength={255}
/>
</div>
<div className="page-block-section relative -mt-2 text-custom-text-200">
<div
id="tiptap-container"
className="page-block-section relative -mt-2 text-custom-text-200"
>
<Controller
name="description_html"
control={control}
@@ -292,7 +295,6 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
if (!data)
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={"<p></p>"}
debouncedUpdatesEnabled={false}
@@ -312,11 +314,12 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
return (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={
value && value !== "" && Object.keys(value).length > 0
? value
: watch("description_html") && watch("description_html") !== ""
? watch("description_html")
: { type: "doc", content: [{ type: "paragraph" }] }
}
debouncedUpdatesEnabled={false}
@@ -334,8 +337,9 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
<div className="m-2 mt-6 flex">
<button
type="button"
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
}`}
className={`flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-80 ${
iAmFeelingLucky ? "cursor-wait bg-custom-background-90" : ""
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
>
@@ -367,8 +371,8 @@ export const CreateUpdateBlockInline: React.FC<Props> = ({
? "Updating..."
: "Update block"
: isSubmitting
? "Adding..."
: "Add block"}
? "Adding..."
: "Add block"}
</PrimaryButton>
</div>
</form>
@@ -456,7 +456,6 @@ export const SinglePageBlock: React.FC<Props> = ({
{showBlockDetails
? block.description_html.length > 7 && (
<TiptapEditor
workspaceSlug={workspaceSlug as string}
value={block.description_html}
customClassName="text-sm min-h-[150px]"
noBorder
@@ -1,7 +1,7 @@
// ui
import { BarGraph, ProfileEmptyState, Loader } from "components/ui";
// image
import emptyBarGraph from "public/empty-state/empty_bar_graph.svg";
import priorityGraph from "public/empty-state/priority_graph.svg";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
@@ -12,10 +12,10 @@ type Props = {
};
export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) => (
<div className="flex flex-col space-y-2">
<div className="space-y-2">
<h3 className="text-lg font-medium">Issues by Priority</h3>
{userProfile ? (
<div className="flex-grow border border-custom-border-100 rounded">
<div className="border border-custom-border-100 rounded">
{userProfile.priority_distribution.length > 0 ? (
<BarGraph
data={userProfile.priority_distribution.map((priority) => ({
@@ -63,11 +63,11 @@ export const ProfilePriorityDistribution: React.FC<Props> = ({ userProfile }) =>
}}
/>
) : (
<div className="flex-grow p-7">
<div className="p-7">
<ProfileEmptyState
title="No Data yet"
description="Create issues to view the them by priority in the graph for better analysis."
image={emptyBarGraph}
image={priorityGraph}
/>
</div>
)}
@@ -16,9 +16,9 @@ export const ProfileStateDistribution: React.FC<Props> = ({ stateDistribution, u
if (!userProfile) return null;
return (
<div className="flex flex-col space-y-2">
<div className="space-y-2">
<h3 className="text-lg font-medium">Issues by State</h3>
<div className="flex-grow border border-custom-border-100 rounded p-7">
<div className="border border-custom-border-100 rounded p-7">
{userProfile.state_distribution.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<div>
+2 -4
View File
@@ -105,11 +105,9 @@ export const ProfileSidebar = () => {
</div>
<div className="px-5">
<div className="mt-[38px]">
<h4 className="text-lg font-semibold">
{userProjectsData.user_data.first_name} {userProjectsData.user_data.last_name}
</h4>
<h4 className="text-lg font-semibold">{userProjectsData.user_data.display_name}</h4>
<h6 className="text-custom-text-200 text-sm">
({userProjectsData.user_data.display_name})
{userProjectsData.user_data.display_name}
</h6>
</div>
<div className="mt-6 space-y-5">
@@ -42,7 +42,6 @@ import { NETWORK_CHOICES } from "constants/project";
type Props = {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
setToFavorite?: boolean;
user: ICurrentUserResponse | undefined;
};
@@ -75,12 +74,7 @@ const IsGuestCondition: React.FC<{
return null;
};
export const CreateProjectModal: React.FC<Props> = ({
isOpen,
setIsOpen,
setToFavorite = false,
user,
}) => {
export const CreateProjectModal: React.FC<Props> = ({ isOpen, setIsOpen, user }) => {
const [isChangeInIdentifierRequired, setIsChangeInIdentifierRequired] = useState(true);
const { setToastAlert } = useToast();
@@ -110,29 +104,6 @@ export const CreateProjectModal: React.FC<Props> = ({
reset(defaultValues);
};
const handleAddToFavorites = (projectId: string) => {
if (!workspaceSlug) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === projectId ? { ...p, is_favorite: true } : p)),
false
);
projectServices
.addProjectToFavorites(workspaceSlug as string, {
project: projectId,
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug) return;
@@ -154,9 +125,6 @@ export const CreateProjectModal: React.FC<Props> = ({
title: "Success!",
message: "Project created successfully.",
});
if (setToFavorite) {
handleAddToFavorites(res.id);
}
handleClose();
})
.catch((err) => {
@@ -1,11 +1,9 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
@@ -29,11 +27,6 @@ type TConfirmProjectDeletionProps = {
user: ICurrentUserResponse | undefined;
};
const defaultValues = {
projectName: "",
confirmDelete: "",
};
export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
isOpen,
data,
@@ -41,41 +34,51 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
onSuccess,
user,
}) => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmProjectName, setConfirmProjectName] = useState("");
const [confirmDeleteMyProject, setConfirmDeleteMyProject] = useState(false);
const [selectedProject, setSelectedProject] = useState<IProject | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { setToastAlert } = useToast();
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues });
const canDelete = confirmProjectName === data?.name && confirmDeleteMyProject;
const canDelete =
watch("projectName") === data?.name && watch("confirmDelete") === "delete my project";
useEffect(() => {
if (data) setSelectedProject(data);
else {
const timer = setTimeout(() => {
setSelectedProject(null);
clearTimeout(timer);
}, 300);
}
}, [data]);
const handleClose = () => {
setIsDeleteLoading(false);
const timer = setTimeout(() => {
reset(defaultValues);
setConfirmProjectName("");
setConfirmDeleteMyProject(false);
clearTimeout(timer);
}, 350);
onClose();
};
const onSubmit = async () => {
const handleDeletion = async () => {
if (!data || !workspaceSlug || !canDelete) return;
setIsDeleteLoading(true);
await projectService
.deleteProject(workspaceSlug.toString(), data.id, user)
.deleteProject(workspaceSlug as string, data.id, user)
.then(() => {
handleClose();
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug.toString(), { is_favorite: "all" }),
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) => prevData?.filter((project: IProject) => project.id !== data.id),
false
);
@@ -88,7 +91,8 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
title: "Error!",
message: "Something went wrong. Please try again later.",
})
);
)
.finally(() => setIsDeleteLoading(false));
};
return (
@@ -118,7 +122,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
@@ -133,29 +137,28 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span>
<p className="text-sm leading-7 text-custom-text-200">
Are you sure you want to delete project{" "}
<span className="break-words font-semibold">{data?.name}</span>? All of the
data related to the project will be permanently removed. This action cannot be
undone
<span className="break-words font-semibold">{selectedProject?.name}</span>?
All of the data related to the project will be permanently removed. This
action cannot be undone
</p>
</span>
<div className="text-custom-text-200">
<p className="break-words text-sm ">
Enter the project name{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span> to
continue:
<span className="font-medium text-custom-text-100">
{selectedProject?.name}
</span>{" "}
to continue:
</p>
<Controller
control={control}
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={confirmProjectName}
onChange={(e) => {
setConfirmProjectName(e.target.value);
}}
name="projectName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Project name"
className="mt-2"
value={value}
onChange={onChange}
/>
)}
/>
</div>
<div className="text-custom-text-200">
@@ -164,27 +167,31 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
<span className="font-medium text-custom-text-100">delete my project</span>{" "}
below:
</p>
<Controller
control={control}
name="confirmDelete"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Enter 'delete my project'"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
<Input
type="text"
placeholder="Enter 'delete my project'"
className="mt-2"
onChange={(e) => {
if (e.target.value === "delete my project") {
setConfirmDeleteMyProject(true);
} else {
setConfirmDeleteMyProject(false);
}
}}
name="typeDelete"
/>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting..." : "Delete Project"}
<DangerButton
onClick={handleDeletion}
disabled={!canDelete}
loading={isDeleteLoading}
>
{isDeleteLoading ? "Deleting..." : "Delete Project"}
</DangerButton>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
@@ -19,7 +19,6 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
const handleJoin = () => {
setIsJoiningLoading(true);
onJoin()
.then(() => {
setIsJoiningLoading(false);
@@ -60,7 +59,7 @@ export const JoinProjectModal: React.FC<TJoinProjectModalProps> = ({ onClose, on
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 border border-custom-border-300 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-80 px-5 py-8 text-left shadow-xl transition-all sm:w-full sm:max-w-xl sm:p-6">
<div className="space-y-5">
<Dialog.Title
as="h3"
@@ -1,474 +0,0 @@
import React, { useEffect } from "react";
// next imports
import { useRouter } from "next/router";
// react-hook-form
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// ui components
import { ToggleSwitch, PrimaryButton, SecondaryButton } from "components/ui";
import { CustomPopover } from "./popover";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
import { IProjectPublishSettingsViews } from "store/project-publish";
// hooks
import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
type Props = {
// user: ICurrentUserResponse | undefined;
};
const defaultValues: Partial<any> = {
id: null,
comments: false,
reactions: false,
votes: false,
inbox: null,
views: ["list", "kanban"],
};
const viewOptions = [
{ key: "list", value: "List" },
{ key: "kanban", value: "Kanban" },
// { key: "calendar", value: "Calendar" },
// { key: "gantt", value: "Gantt" },
// { key: "spreadsheet", value: "Spreadsheet" },
];
export const PublishProjectModal: React.FC<Props> = observer(() => {
const store: RootStore = useMobxStore();
const { projectPublish } = store;
const { projectDetails, mutateProjectDetails } = useProjectDetails();
const { setToastAlert } = useToast();
const handleToastAlert = (title: string, type: string, message: string) => {
setToastAlert({
title: title || "Title",
type: "error" || "warning",
message: message || "Message",
});
};
const { NEXT_PUBLIC_DEPLOY_URL } = process.env;
const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL
? NEXT_PUBLIC_DEPLOY_URL
: "http://localhost:3001";
const router = useRouter();
const { workspaceSlug } = router.query;
const {
formState: { errors, isSubmitting },
handleSubmit,
reset,
watch,
setValue,
} = useForm<any>({
defaultValues,
reValidateMode: "onChange",
});
const handleClose = () => {
projectPublish.handleProjectModal(null);
reset({ ...defaultValues });
};
useEffect(() => {
if (
projectPublish.projectPublishSettings &&
projectPublish.projectPublishSettings != "not-initialized"
) {
let userBoards: string[] = [];
if (projectPublish.projectPublishSettings?.views) {
const _views: IProjectPublishSettingsViews | null =
projectPublish.projectPublishSettings?.views || null;
if (_views != null) {
if (_views.list) userBoards.push("list");
if (_views.kanban) userBoards.push("kanban");
if (_views.calendar) userBoards.push("calendar");
if (_views.gantt) userBoards.push("gantt");
if (_views.spreadsheet) userBoards.push("spreadsheet");
userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"];
}
}
const updatedData = {
id: projectPublish.projectPublishSettings?.id || null,
comments: projectPublish.projectPublishSettings?.comments || false,
reactions: projectPublish.projectPublishSettings?.reactions || false,
votes: projectPublish.projectPublishSettings?.votes || false,
inbox: projectPublish.projectPublishSettings?.inbox || null,
views: userBoards,
};
reset({ ...updatedData });
}
}, [reset, projectPublish.projectPublishSettings]);
useEffect(() => {
if (
projectPublish.projectPublishModal &&
workspaceSlug &&
projectPublish.project_id != null &&
projectPublish?.projectPublishSettings === "not-initialized"
) {
projectPublish.getProjectSettingsAsync(
workspaceSlug as string,
projectPublish.project_id as string,
null
);
}
}, [workspaceSlug, projectPublish, projectPublish.projectPublishModal]);
const onSettingsPublish = async (formData: any) => {
if (formData.views && formData.views.length > 0) {
const payload = {
comments: formData.comments || false,
reactions: formData.reactions || false,
votes: formData.votes || false,
inbox: formData.inbox || null,
views: {
list: formData.views.includes("list") || false,
kanban: formData.views.includes("kanban") || false,
calendar: formData.views.includes("calendar") || false,
gantt: formData.views.includes("gantt") || false,
spreadsheet: formData.views.includes("spreadsheet") || false,
},
};
const _workspaceSlug = workspaceSlug;
const _projectId = projectPublish.project_id;
return projectPublish
.createProjectSettingsAsync(_workspaceSlug as string, _projectId as string, payload, null)
.then((response) => {
mutateProjectDetails();
handleClose();
console.log("_projectId", _projectId);
if (_projectId)
window.open(`${plane_deploy_url}/${_workspaceSlug}/${_projectId}`, "_blank");
return response;
})
.catch((error) => {
console.error("error", error);
return error;
});
} else {
handleToastAlert("Missing fields", "warning", "Please select at least one view to publish");
}
};
const onSettingsUpdate = async (key: string, value: any) => {
const payload = {
comments: key === "comments" ? value : watch("comments"),
reactions: key === "reactions" ? value : watch("reactions"),
votes: key === "votes" ? value : watch("votes"),
inbox: key === "inbox" ? value : watch("inbox"),
views:
key === "views"
? {
list: value.includes("list") ? true : false,
kanban: value.includes("kanban") ? true : false,
calendar: value.includes("calendar") ? true : false,
gantt: value.includes("gantt") ? true : false,
spreadsheet: value.includes("spreadsheet") ? true : false,
}
: {
list: watch("views").includes("list") ? true : false,
kanban: watch("views").includes("kanban") ? true : false,
calendar: watch("views").includes("calendar") ? true : false,
gantt: watch("views").includes("gantt") ? true : false,
spreadsheet: watch("views").includes("spreadsheet") ? true : false,
},
};
return projectPublish
.updateProjectSettingsAsync(
workspaceSlug as string,
projectPublish.project_id as string,
watch("id"),
payload,
null
)
.then((response) => {
mutateProjectDetails();
return response;
})
.catch((error) => {
console.log("error", error);
return error;
});
};
const onSettingsUnPublish = async (formData: any) =>
projectPublish
.deleteProjectSettingsAsync(
workspaceSlug as string,
projectPublish.project_id as string,
formData?.id,
null
)
.then((response) => {
mutateProjectDetails();
reset({ ...defaultValues });
handleClose();
return response;
})
.catch((error) => {
console.error("error", error);
return error;
});
const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => {
const [status, setStatus] = React.useState(false);
const copyText = () => {
navigator.clipboard.writeText(copy_link);
setStatus(true);
setTimeout(() => {
setStatus(false);
}, 1000);
};
return (
<div
className="border border-custom-border-100 bg-custom-background-100 text-xs px-2 min-w-[30px] h-[30px] rounded flex justify-center items-center hover:bg-custom-background-90 cursor-pointer"
onClick={() => copyText()}
>
{status ? "Copied" : "Copy Link"}
</div>
);
};
return (
<Transition.Root show={projectPublish.projectPublishModal} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-200"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="transform rounded-lg bg-custom-background-100 border border-custom-border-100 text-left shadow-xl transition-all w-full sm:w-3/5 lg:w-1/2 xl:w-2/5 space-y-4">
{/* heading */}
<div className="p-3 px-4 pb-0 flex gap-2 justify-between items-center">
<div className="font-medium text-xl">Publish</div>
{projectPublish.loader && (
<div className="text-xs text-custom-text-400">Changes saved</div>
)}
<div
className="hover:bg-custom-background-90 w-[30px] h-[30px] rounded flex justify-center items-center cursor-pointer transition-all"
onClick={handleClose}
>
<span className="material-symbols-rounded text-[16px]">close</span>
</div>
</div>
{/* content */}
<div className="space-y-3">
{watch("id") && (
<div className="flex items-center gap-1 px-4 text-custom-primary-100">
<div className="w-[20px] h-[20px] overflow-hidden flex items-center">
<span className="material-symbols-rounded text-[18px]">
radio_button_checked
</span>
</div>
<div className="text-sm">This project is live on web</div>
</div>
)}
<div className="mx-4 border border-custom-border-100 bg-custom-background-90 rounded p-3 py-2 relative flex gap-2 items-center">
<div className="relative line-clamp-1 overflow-hidden w-full text-sm">
{`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`}
</div>
<div className="flex-shrink-0 relative flex items-center gap-1">
<a
href={`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`}
target="_blank"
rel="noreferrer"
>
<div className="border border-custom-border-100 bg-custom-background-100 w-[30px] h-[30px] rounded flex justify-center items-center hover:bg-custom-background-90 cursor-pointer">
<span className="material-symbols-rounded text-[16px]">open_in_new</span>
</div>
</a>
<CopyLinkToClipboard
copy_link={`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`}
/>
</div>
</div>
<div className="space-y-3 px-4">
<div className="relative flex justify-between items-center gap-2">
<div className="text-custom-text-100">Views</div>
<div>
<CustomPopover
label={
watch("views") && watch("views").length > 0
? viewOptions
.filter(
(_view) => watch("views").includes(_view.key) && _view.value
)
.map((_view) => _view.value)
.join(", ")
: ``
}
placeholder="Select views"
>
<>
{viewOptions &&
viewOptions.length > 0 &&
viewOptions.map((_view) => (
<div
key={_view.value}
className={`relative flex items-center gap-2 justify-between p-1 m-1 px-2 cursor-pointer rounded-sm text-custom-text-200 ${
watch("views").includes(_view.key)
? `bg-custom-background-80 text-custom-text-100`
: `hover:bg-custom-background-80 hover:text-custom-text-100`
}`}
onClick={() => {
const _views =
watch("views") && watch("views").length > 0
? watch("views").includes(_view?.key)
? watch("views").filter((_o: string) => _o !== _view?.key)
: [...watch("views"), _view?.key]
: [_view?.key];
setValue("views", _views);
if (watch("id") != null) onSettingsUpdate("views", _views);
}}
>
<div className="text-sm">{_view.value}</div>
<div
className={`w-[18px] h-[18px] relative flex justify-center items-center`}
>
{watch("views") &&
watch("views").length > 0 &&
watch("views").includes(_view.key) && (
<span className="material-symbols-rounded text-[18px]">
done
</span>
)}
</div>
</div>
))}
</>
</CustomPopover>
</div>
</div>
{/* <div className="relative flex justify-between items-center gap-2">
<div className="text-custom-text-100">Allow comments</div>
<div>
<ToggleSwitch
value={watch("comments") ?? false}
onChange={() => {
const _comments = !watch("comments");
setValue("comments", _comments);
if (watch("id") != null) onSettingsUpdate("comments", _comments);
}}
size="sm"
/>
</div>
</div> */}
{/* <div className="relative flex justify-between items-center gap-2">
<div className="text-custom-text-100">Allow reactions</div>
<div>
<ToggleSwitch
value={watch("reactions") ?? false}
onChange={() => {
const _reactions = !watch("reactions");
setValue("reactions", _reactions);
if (watch("id") != null) onSettingsUpdate("reactions", _reactions);
}}
size="sm"
/>
</div>
</div> */}
{/* <div className="relative flex justify-between items-center gap-2">
<div className="text-custom-text-100">Allow Voting</div>
<div>
<ToggleSwitch
value={watch("votes") ?? false}
onChange={() => {
const _votes = !watch("votes");
setValue("votes", _votes);
if (watch("id") != null) onSettingsUpdate("votes", _votes);
}}
size="sm"
/>
</div>
</div> */}
{/* <div className="relative flex justify-between items-center gap-2">
<div className="text-custom-text-100">Allow issue proposals</div>
<div>
<ToggleSwitch
value={watch("inbox") ?? false}
onChange={() => {
setValue("inbox", !watch("inbox"));
}}
size="sm"
/>
</div>
</div> */}
</div>
</div>
{/* modal handlers */}
<div className="border-t border-custom-border-300 p-3 px-4 relative flex justify-between items-center">
<div className="flex items-center gap-1 text-custom-text-300">
<div className="w-[20px] h-[20px] overflow-hidden flex items-center">
<span className="material-symbols-rounded text-[18px]">public</span>
</div>
<div className="text-sm">Anyone with the link can access</div>
</div>
<div className="relative flex items-center gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
{watch("id") != null ? (
<PrimaryButton
outline
onClick={handleSubmit(onSettingsUnPublish)}
disabled={isSubmitting}
>
{isSubmitting ? "Unpublishing..." : "Unpublish"}
</PrimaryButton>
) : (
<PrimaryButton
onClick={handleSubmit(onSettingsPublish)}
disabled={isSubmitting}
>
{isSubmitting ? "Publishing..." : "Publish"}
</PrimaryButton>
)}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
});
@@ -1,54 +0,0 @@
import React, { Fragment } from "react";
// headless ui
import { Popover, Transition } from "@headlessui/react";
export const CustomPopover = ({
children,
label,
placeholder = "Select",
}: {
children: React.ReactNode;
label?: string;
placeholder?: string;
}) => (
<div className="relative">
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={`${
open ? "" : ""
} relative flex items-center gap-1 border border-custom-border-300 shadow-sm p-1 px-2 ring-0 outline-none`}
>
<div className="text-sm font-medium">
{label ? label : placeholder ? placeholder : "Select"}
</div>
<div className="w-[20px] h-[20px] relative flex justify-center items-center">
{!open ? (
<span className="material-symbols-rounded text-[20px]">expand_more</span>
) : (
<span className="material-symbols-rounded text-[20px]">expand_less</span>
)}
</div>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute right-0 z-[9999]">
<div className="overflow-hidden rounded-sm border border-custom-border-300 mt-1 overflow-y-auto bg-custom-background-90 shadow-lg focus:outline-none">
{children}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</div>
);
+25 -33
View File
@@ -9,10 +9,11 @@ import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautif
import { Disclosure, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
import useTheme from "hooks/use-theme";
import useUserAuth from "hooks/use-user-auth";
import useProjects from "hooks/use-projects";
// components
import { CreateProjectModal, DeleteProjectModal, SingleSidebarProject } from "components/project";
import { DeleteProjectModal, SingleSidebarProject } from "components/project";
// services
import projectService from "services/project.service";
// icons
@@ -31,7 +32,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
export const ProjectSidebarList: FC = () => {
const store: any = useMobxStore();
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
const [deleteProjectModal, setDeleteProjectModal] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<IProject | null>(null);
@@ -41,15 +41,18 @@ export const ProjectSidebarList: FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { user } = useUserAuth();
const { collapsed: sidebarCollapse } = useTheme();
const { setToastAlert } = useToast();
const { projects: allProjects } = useProjects();
const joinedProjects = allProjects?.filter((p) => p.is_member);
const joinedProjects = allProjects?.filter((p) => p.sort_order);
const favoriteProjects = allProjects?.filter((p) => p.is_favorite);
const otherProjects = allProjects?.filter((p) => p.sort_order === null);
const orderedJoinedProjects: IProject[] | undefined = joinedProjects
? orderArrayBy(joinedProjects, "sort_order", "ascending")
@@ -148,12 +151,6 @@ export const ProjectSidebarList: FC = () => {
return (
<>
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
setToFavorite
user={user}
/>
<DeleteProjectModal
isOpen={deleteProjectModal}
onClose={() => setDeleteProjectModal(false)}
@@ -175,25 +172,17 @@ export const ProjectSidebarList: FC = () => {
{({ open }) => (
<>
{!store?.theme?.sidebarCollapsed && (
<div className="group flex justify-between items-center text-xs px-1.5 rounded text-custom-sidebar-text-400 hover:bg-custom-sidebar-background-80 w-full">
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-400 text-left hover:bg-custom-sidebar-background-80 rounded w-full whitespace-nowrap"
>
Favorites
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</Disclosure.Button>
<button
className="group-hover:opacity-100 opacity-0"
onClick={() => setIsProjectModalOpen(true)}
>
<Icon iconName="add" />
</button>
</div>
<Disclosure.Button
as="button"
type="button"
className="group flex items-center gap-1 px-1.5 text-xs font-semibold text-custom-sidebar-text-400 text-left hover:bg-custom-sidebar-background-80 rounded w-full whitespace-nowrap"
>
Favorites
<Icon
iconName={open ? "arrow_drop_down" : "arrow_right"}
className="group-hover:opacity-100 opacity-0 !text-lg"
/>
</Disclosure.Button>
)}
<Disclosure.Panel as="div" className="space-y-2">
{orderedFavProjects.map((project, index) => (
@@ -201,7 +190,7 @@ export const ProjectSidebarList: FC = () => {
key={project.id}
draggableId={project.id}
index={index}
isDragDisabled={!project.is_member}
isDragDisabled={project.sort_order === null}
>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
@@ -252,7 +241,10 @@ export const ProjectSidebarList: FC = () => {
</Disclosure.Button>
<button
className="group-hover:opacity-100 opacity-0"
onClick={() => setIsProjectModalOpen(true)}
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "p" });
document.dispatchEvent(e);
}}
>
<Icon iconName="add" />
</button>
@@ -296,10 +288,10 @@ export const ProjectSidebarList: FC = () => {
</Droppable>
</DragDropContext>
{joinedProjects && joinedProjects.length === 0 && (
{allProjects && allProjects.length === 0 && (
<button
type="button"
className="flex w-full items-center gap-2 px-3 text-sm text-custom-sidebar-text-200"
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-custom-sidebar-text-200 mt-5"
onClick={() => {
const e = new KeyboardEvent("keydown", {
key: "p",
@@ -32,11 +32,6 @@ import { renderEmoji } from "helpers/emoji.helper";
import { IProject } from "types";
// fetch-keys
import { PROJECTS_LIST } from "constants/fetch-keys";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
type Props = {
project: IProject;
@@ -81,277 +76,252 @@ const navigation = (workspaceSlug: string, projectId: string) => [
},
];
export const SingleSidebarProject: React.FC<Props> = observer(
({
project,
sidebarCollapse,
provided,
snapshot,
handleDeleteProject,
handleCopyText,
shortContextMenu = false,
}) => {
const store: RootStore = useMobxStore();
const { projectPublish } = store;
export const SingleSidebarProject: React.FC<Props> = ({
project,
sidebarCollapse,
provided,
snapshot,
handleDeleteProject,
handleCopyText,
shortContextMenu = false,
}) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { setToastAlert } = useToast();
const isAdmin = project.member_role === 20;
const isAdmin = project.member_role === 20;
const handleAddToFavorites = () => {
if (!workspaceSlug) return;
const handleAddToFavorites = () => {
if (!workspaceSlug) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
false
);
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: true } : p)),
false
);
projectService
.addProjectToFavorites(workspaceSlug as string, {
project: project.id,
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
};
const handleRemoveFromFavorites = () => {
if (!workspaceSlug) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
false
);
projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() =>
projectService
.addProjectToFavorites(workspaceSlug as string, {
project: project.id,
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
};
};
return (
<Disclosure key={project.id} defaultOpen={projectId === project.id}>
{({ open }) => (
<>
<div
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
snapshot?.isDragging ? "opacity-60" : ""
}`}
>
{provided && (
<Tooltip
tooltipContent={
project.sort_order === null
? "Join the project to rearrange"
: "Drag to rearrange"
}
position="top-right"
>
<button
type="button"
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 text-custom-sidebar-text-400 ${
sidebarCollapse ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
{...provided?.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
</Tooltip>
)}
const handleRemoveFromFavorites = () => {
if (!workspaceSlug) return;
mutate<IProject[]>(
PROJECTS_LIST(workspaceSlug as string, { is_favorite: "all" }),
(prevData) =>
(prevData ?? []).map((p) => (p.id === project.id ? { ...p, is_favorite: false } : p)),
false
);
projectService.removeProjectFromFavorites(workspaceSlug as string, project.id).catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the project from favorites. Please try again.",
})
);
};
return (
<Disclosure key={project.id} defaultOpen={projectId === project.id}>
{({ open }) => (
<>
<div
className={`group relative text-custom-sidebar-text-10 px-2 py-1 w-full flex items-center hover:bg-custom-sidebar-background-80 rounded-md ${
snapshot?.isDragging ? "opacity-60" : ""
}`}
>
{provided && (
<Tooltip
tooltipContent={`${project.name}`}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
tooltipContent={
project.sort_order === null
? "Join the project to rearrange"
: "Drag to rearrange"
}
position="top-right"
>
<Disclosure.Button
as="div"
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : `justify-between`
<button
type="button"
className={`absolute top-1/2 -translate-y-1/2 -left-4 hidden rounded p-0.5 text-custom-sidebar-text-400 ${
sidebarCollapse ? "" : "group-hover:!flex"
} ${project.sort_order === null ? "opacity-60 cursor-not-allowed" : ""}`}
{...provided?.dragHandleProps}
>
<EllipsisVerticalIcon className="h-4" />
<EllipsisVerticalIcon className="-ml-5 h-4" />
</button>
</Tooltip>
)}
<Tooltip
tooltipContent={`${project.name}`}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
>
<Disclosure.Button
as="div"
className={`flex items-center flex-grow truncate cursor-pointer select-none text-left text-sm font-medium ${
sidebarCollapse ? "justify-center" : `justify-between`
}`}
>
<div
className={`flex items-center flex-grow w-full truncate gap-x-2 ${
sidebarCollapse ? "justify-center" : ""
}`}
>
<div
className={`flex items-center flex-grow w-full truncate gap-x-2 ${
sidebarCollapse ? "justify-center" : ""
}`}
>
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-7 w-7 flex-shrink-0 grid place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
{project.emoji ? (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
{renderEmoji(project.emoji)}
</span>
) : project.icon_prop ? (
<div className="h-7 w-7 flex-shrink-0 grid place-items-center">
{renderEmoji(project.icon_prop)}
</div>
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
{project?.name.charAt(0)}
</span>
)}
{!sidebarCollapse && (
<p className={`truncate ${open ? "" : "text-custom-sidebar-text-200"}`}>
{project.name}
</p>
)}
</div>
{!sidebarCollapse && (
<ExpandMoreOutlined
fontSize="small"
className={`flex-shrink-0 ${
open ? "rotate-180" : ""
} !hidden group-hover:!block text-custom-sidebar-text-400 duration-300`}
/>
<p className={`truncate ${open ? "" : "text-custom-sidebar-text-200"}`}>
{project.name}
</p>
)}
</Disclosure.Button>
</Tooltip>
</div>
{!sidebarCollapse && (
<ExpandMoreOutlined
fontSize="small"
className={`flex-shrink-0 ${
open ? "rotate-180" : ""
} !hidden group-hover:!block text-custom-sidebar-text-400 duration-300`}
/>
)}
</Disclosure.Button>
</Tooltip>
{!sidebarCollapse && (
<CustomMenu
className="hidden group-hover:block flex-shrink-0"
buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400"
ellipsis
>
{!shortContextMenu && isAdmin && (
<CustomMenu.MenuItem onClick={handleDeleteProject}>
<span className="flex items-center justify-start gap-2 ">
<TrashIcon className="h-4 w-4" />
<span>Delete project</span>
</span>
</CustomMenu.MenuItem>
)}
{!project.is_favorite && (
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
<span className="flex items-center justify-start gap-2">
<StarIcon className="h-4 w-4" />
<span>Add to favorites</span>
</span>
</CustomMenu.MenuItem>
)}
{project.is_favorite && (
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
<span className="flex items-center justify-start gap-2">
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
<span>Remove from favorites</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy project link</span>
{!sidebarCollapse && (
<CustomMenu
className="hidden group-hover:block flex-shrink-0"
buttonClassName="!text-custom-sidebar-text-400 hover:text-custom-sidebar-text-400"
ellipsis
>
{!shortContextMenu && isAdmin && (
<CustomMenu.MenuItem onClick={handleDeleteProject}>
<span className="flex items-center justify-start gap-2 ">
<TrashIcon className="h-4 w-4" />
<span>Delete project</span>
</span>
</CustomMenu.MenuItem>
{/* publish project settings */}
{isAdmin && (
<CustomMenu.MenuItem
onClick={() => projectPublish.handleProjectModal(project?.id)}
>
<div className="flex-shrink-0 relative flex items-center justify-start gap-2">
<div className="rounded transition-all w-4 h-4 flex justify-center items-center text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 duration-300 cursor-pointer">
<span className="material-symbols-rounded text-[16px]">ios_share</span>
</div>
<div>Publish</div>
</div>
{/* <PublishProjectModal /> */}
</CustomMenu.MenuItem>
)}
{project.archive_in > 0 && (
<CustomMenu.MenuItem
onClick={() =>
router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)
}
>
<div className="flex items-center justify-start gap-2">
<ArchiveOutlined fontSize="small" />
<span>Archived Issues</span>
</div>
</CustomMenu.MenuItem>
)}
)}
{!project.is_favorite && (
<CustomMenu.MenuItem onClick={handleAddToFavorites}>
<span className="flex items-center justify-start gap-2">
<StarIcon className="h-4 w-4" />
<span>Add to favorites</span>
</span>
</CustomMenu.MenuItem>
)}
{project.is_favorite && (
<CustomMenu.MenuItem onClick={handleRemoveFromFavorites}>
<span className="flex items-center justify-start gap-2">
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
<span>Remove from favorites</span>
</span>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy project link</span>
</span>
</CustomMenu.MenuItem>
{project.archive_in > 0 && (
<CustomMenu.MenuItem
onClick={() =>
router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)
router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)
}
>
<div className="flex items-center justify-start gap-2">
<Icon iconName="settings" className="!text-base !leading-4" />
<span>Settings</span>
<ArchiveOutlined fontSize="small" />
<span>Archived Issues</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
)}
<CustomMenu.MenuItem
onClick={() => router.push(`/${workspaceSlug}/projects/${project?.id}/settings`)}
>
<div className="flex items-center justify-start gap-2">
<Icon iconName="settings" className="!text-base !leading-4" />
<span>Settings</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel
className={`space-y-2 mt-1 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}
>
{navigation(workspaceSlug as string, project?.id).map((item) => {
if (
(item.name === "Cycles" && !project.cycle_view) ||
(item.name === "Modules" && !project.module_view) ||
(item.name === "Views" && !project.issue_views_view) ||
(item.name === "Pages" && !project.page_view)
)
return;
<Transition
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Disclosure.Panel className={`space-y-2 mt-1 ${sidebarCollapse ? "" : "ml-[2.25rem]"}`}>
{navigation(workspaceSlug as string, project?.id).map((item) => {
if (
(item.name === "Cycles" && !project.cycle_view) ||
(item.name === "Modules" && !project.module_view) ||
(item.name === "Views" && !project.issue_views_view) ||
(item.name === "Pages" && !project.page_view)
)
return;
return (
<Link key={item.name} href={item.href}>
<a className="block w-full">
<Tooltip
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
return (
<Link key={item.name} href={item.href}>
<a className="block w-full">
<Tooltip
tooltipContent={`${project?.name}: ${item.name}`}
position="right"
className="ml-2"
disabled={!sidebarCollapse}
>
<div
className={`group flex items-center rounded-md px-2 py-1.5 gap-2.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<div
className={`group flex items-center rounded-md px-2 py-1.5 gap-2.5 text-xs font-medium outline-none ${
router.asPath.includes(item.href)
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80"
} ${sidebarCollapse ? "justify-center" : ""}`}
>
<item.Icon
sx={{
fontSize: 18,
}}
/>
{!sidebarCollapse && item.name}
</div>
</Tooltip>
</a>
</Link>
);
})}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
);
}
);
<item.Icon
sx={{
fontSize: 18,
}}
/>
{!sidebarCollapse && item.name}
</div>
</Tooltip>
</a>
</Link>
);
})}
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
);
};
@@ -15,36 +15,36 @@ export interface BubbleMenuItem {
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
const items: BubbleMenuItem[] = [
{
name: "bold",
isActive: () => props.editor?.isActive("bold"),
command: () => props.editor?.chain().focus().toggleBold().run(),
isActive: () => props.editor.isActive("bold"),
command: () => props.editor.chain().focus().toggleBold().run(),
icon: BoldIcon,
},
{
name: "italic",
isActive: () => props.editor?.isActive("italic"),
command: () => props.editor?.chain().focus().toggleItalic().run(),
isActive: () => props.editor.isActive("italic"),
command: () => props.editor.chain().focus().toggleItalic().run(),
icon: ItalicIcon,
},
{
name: "underline",
isActive: () => props.editor?.isActive("underline"),
command: () => props.editor?.chain().focus().toggleUnderline().run(),
isActive: () => props.editor.isActive("underline"),
command: () => props.editor.chain().focus().toggleUnderline().run(),
icon: UnderlineIcon,
},
{
name: "strike",
isActive: () => props.editor?.isActive("strike"),
command: () => props.editor?.chain().focus().toggleStrike().run(),
isActive: () => props.editor.isActive("strike"),
command: () => props.editor.chain().focus().toggleStrike().run(),
icon: StrikethroughIcon,
},
{
name: "code",
isActive: () => props.editor?.isActive("code"),
command: () => props.editor?.chain().focus().toggleCode().run(),
isActive: () => props.editor.isActive("code"),
command: () => props.editor.chain().focus().toggleCode().run(),
icon: CodeIcon,
},
];
@@ -78,7 +78,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
>
<NodeSelector
editor={props.editor!}
editor={props.editor}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
@@ -86,7 +86,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
}}
/>
<LinkSelector
editor={props.editor!!}
editor={props.editor}
isOpen={isLinkSelectorOpen}
setIsOpen={() => {
setIsLinkSelectorOpen(!isLinkSelectorOpen);
@@ -97,14 +97,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
{items.map((item, index) => (
<button
key={index}
type="button"
onClick={item.command}
className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors",
{
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
}
)}
className={cn("p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(),
})}
>
<item.icon
className={cn("h-4 w-4", {
@@ -1,27 +1,17 @@
import { Editor } from "@tiptap/core";
import { Check, Trash } from "lucide-react";
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
import { cn } from "../utils";
import isValidHttpUrl from "./utils/link-validator";
interface LinkSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => {
const input = inputRef.current;
const url = input?.value;
if (url && isValidHttpUrl(url)) {
editor.chain().focus().setLink({ href: url }).run();
setIsOpen(false);
}
}, [editor, inputRef, setIsOpen]);
useEffect(() => {
inputRef.current && inputRef.current?.focus();
});
@@ -29,11 +19,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
return (
<div className="relative">
<button
type="button"
className={cn(
"flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100",
{ "bg-custom-background-100": isOpen }
)}
className={cn("flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100", { "bg-custom-background-100": isOpen })}
onClick={() => {
setIsOpen(!isOpen);
}}
@@ -48,24 +34,25 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
</p>
</button>
{isOpen && (
<div
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); onLinkSubmit();
}
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const input = form.elements[0] as HTMLInputElement;
editor.chain().focus().setLink({ href: input.value }).run();
setIsOpen(false);
}}
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
>
<input
ref={inputRef}
type="url"
placeholder="Paste a link"
className="flex-1 bg-custom-background-100 border-r border-custom-border-300 p-1 text-sm outline-none placeholder:text-custom-text-400"
className="flex-1 bg-custom-background-100 border border-custom-primary-300 p-1 text-sm outline-none placeholder:text-custom-text-400"
defaultValue={editor.getAttributes("link").href || ""}
/>
{editor.getAttributes("link").href ? (
<button
type="button"
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
onClick={() => {
editor.chain().focus().unsetLink().run();
@@ -75,15 +62,11 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
<Trash className="h-4 w-4" />
</button>
) : (
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90" type="button"
onClick={() => {
onLinkSubmit();
}}
>
<button className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90">
<Check className="h-4 w-4" />
</button>
)}
</div>
</form>
)}
</div>
);
@@ -91,9 +91,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
return (
<div className="relative h-full">
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5"
onClick={() => setIsOpen(!isOpen)}
>
<span>{activeItem?.name}</span>
<ChevronDown className="h-4 w-4" />
@@ -104,15 +103,11 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
{items.map((item, index) => (
<button
key={index}
type="button"
onClick={() => {
item.command();
setIsOpen(false);
}}
className={cn(
"flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100",
{ "bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name }
)}
className={cn("flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100", { "bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name })}
>
<div className="flex items-center space-x-2">
<div className="rounded-sm border border-custom-border-300 p-1">
@@ -1,12 +0,0 @@
export default function isValidHttpUrl(string: string): boolean {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
}
@@ -1,57 +0,0 @@
import { Editor } from "@tiptap/react";
import Moveable from "react-moveable";
export const ImageResizer = ({ editor }: { editor: Editor }) => {
const updateMediaSize = () => {
const imageInfo = document.querySelector(
".ProseMirror-selectednode",
) as HTMLImageElement;
if (imageInfo) {
const selection = editor.state.selection;
editor.commands.setImage({
src: imageInfo.src,
width: Number(imageInfo.style.width.replace("px", "")),
height: Number(imageInfo.style.height.replace("px", "")),
} as any);
editor.commands.setNodeSelection(selection.from);
}
};
return (
<>
<Moveable
target={document.querySelector(".ProseMirror-selectednode") as any}
container={null}
origin={false}
edge={false}
throttleDrag={0}
keepRatio={true}
resizable={true}
throttleResize={0}
onResize={({
target,
width,
height,
delta,
}:
any) => {
delta[0] && (target!.style.width = `${width}px`);
delta[1] && (target!.style.height = `${height}px`);
}}
onResizeEnd={() => {
updateMediaSize();
}}
scalable={true}
renderDirections={["w", "e"]}
onScale={({
target,
transform,
}:
any) => {
target!.style.transform = transform;
}}
/>
</>
);
};
@@ -1,6 +1,7 @@
import StarterKit from "@tiptap/starter-kit";
import HorizontalRule from "@tiptap/extension-horizontal-rule";
import TiptapLink from "@tiptap/extension-link";
import TiptapImage from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder";
import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style";
@@ -17,13 +18,18 @@ import { InputRule } from "@tiptap/core";
import ts from "highlight.js/lib/languages/typescript";
import "highlight.js/styles/github-dark.css";
import UploadImagesPlugin from "../plugins/upload-image";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image";
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
lowlight.registerLanguage("ts", ts);
export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [
const CustomImage = TiptapImage.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin()];
},
});
export const TiptapExtensions = [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -87,14 +93,13 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
},
}),
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
HTMLAttributes: {
class:
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
},
}),
UpdatedImage.configure({
CustomImage.configure({
allowBase64: true,
HTMLAttributes: {
class: "rounded-lg border border-custom-border-300",
},
@@ -112,7 +117,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
UniqueID.configure({
types: ["image"],
}),
SlashCommand(workspaceSlug, setIsSubmitting),
SlashCommand,
TiptapUnderline,
TextStyle,
Color,
@@ -1,22 +0,0 @@
import Image from "@tiptap/extension-image";
import TrackImageDeletionPlugin from "../plugins/delete-image";
import UploadImagesPlugin from "../plugins/upload-image";
const UpdatedImage = Image.extend({
addProseMirrorPlugins() {
return [UploadImagesPlugin(), TrackImageDeletionPlugin()];
},
addAttributes() {
return {
...this.parent?.(),
width: {
default: '35%',
},
height: {
default: null,
},
};
},
});
export default UpdatedImage;
+52 -14
View File
@@ -1,10 +1,14 @@
// @ts-nocheck
import { useEditor, EditorContent, Editor } from "@tiptap/react";
import { useDebouncedCallback } from "use-debounce";
import { EditorBubbleMenu } from "./bubble-menu";
import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
import { useImperativeHandle, useRef } from "react";
import { ImageResizer } from "./extensions/image-resize";
import { Node } from "@tiptap/pm/model";
import { Editor as CoreEditor } from "@tiptap/core";
import { useCallback, useImperativeHandle, useRef } from "react";
import { EditorState } from "@tiptap/pm/state";
import fileService from "services/file.service";
export interface ITiptapRichTextEditor {
value: string;
@@ -14,8 +18,6 @@ export interface ITiptapRichTextEditor {
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
workspaceSlug: string;
editable?: boolean;
forwardedRef?: any;
debouncedUpdatesEnabled?: boolean;
@@ -28,24 +30,22 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
forwardedRef,
editable,
setIsSubmitting,
setShouldShowAlert,
editorContentCustomClassNames,
value,
noBorder,
workspaceSlug,
borderOnFocus,
customClassName,
} = props;
const editor = useEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
extensions: TiptapExtensions(workspaceSlug, setIsSubmitting),
editorProps: TiptapEditorProps,
extensions: TiptapExtensions,
content: value,
onUpdate: async ({ editor }) => {
// for instant feedback loop
setIsSubmitting?.("submitting");
setShouldShowAlert?.(true);
checkForNodeDeletions(editor);
if (debouncedUpdatesEnabled) {
debouncedUpdates({ onChange, editor });
} else {
@@ -65,6 +65,45 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
},
}));
const previousState = useRef<EditorState>();
const onNodeDeleted = useCallback(async (node: Node) => {
if (node.type.name === "image") {
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("file deleted successfully");
}
}
}, []);
const checkForNodeDeletions = useCallback(
(editor: CoreEditor) => {
const prevNodesById: Record<string, Node> = {};
previousState.current?.doc.forEach((node) => {
if (node.attrs.id) {
prevNodesById[node.attrs.id] = node;
}
});
const nodesById: Record<string, Node> = {};
editor.state?.doc.forEach((node) => {
if (node.attrs.id) {
nodesById[node.attrs.id] = node;
}
});
previousState.current = editor.state;
for (const [id, node] of Object.entries(prevNodesById)) {
if (nodesById[id] === undefined) {
onNodeDeleted(node);
}
}
},
[onNodeDeleted]
);
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
setTimeout(async () => {
if (onChange) {
@@ -73,8 +112,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
}, 500);
}, 1000);
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg sm:shadow-lg mt-2 p-3 relative focus:outline-none rounded-md
${noBorder ? '' : 'border border-custom-border-200'
} ${borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0'
} ${customClassName}`;
if (!editor) return null;
@@ -82,16 +122,14 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
return (
<div
id="tiptap-container"
onClick={() => {
editor?.chain().focus().run();
}}
className={`tiptap-editor-container cursor-text ${editorClassNames}`}
className={`tiptap-editor-container ${editorClassNames}`}
>
{editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div>
</div>
);
@@ -1,56 +0,0 @@
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Node as ProseMirrorNode } from '@tiptap/pm/model';
import fileService from "services/file.service";
const deleteKey = new PluginKey("delete-image");
const TrackImageDeletionPlugin = () =>
new Plugin({
key: deleteKey,
appendTransaction: (transactions, oldState, newState) => {
transactions.forEach((transaction) => {
if (!transaction.docChanged) return;
const removedImages: ProseMirrorNode[] = [];
oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== 'image') return;
if (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos);
// Check if the node has been deleted or replaced
if (!newNode || newNode.type.name !== 'image') {
// Check if the node still exists elsewhere in the document
let nodeExists = false;
newState.doc.descendants((node) => {
if (node.attrs.id === oldNode.attrs.id) {
nodeExists = true;
}
});
if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
}
}
});
removedImages.forEach((node) => {
const src = node.attrs.src;
onNodeDeleted(src);
});
});
return null;
},
});
export default TrackImageDeletionPlugin;
async function onNodeDeleted(src: string) {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
if (resStatus === 204) {
console.log("Image deleted successfully");
}
}
@@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) {
return found.length ? found[0].from : null;
}
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
export async function startImageUpload(file: File, view: EditorView, pos: number) {
if (!file.type.includes("image/")) {
return;
} else if (file.size / 1024 / 1024 > 20) {
@@ -82,11 +82,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(tr);
};
if (!workspaceSlug) {
return;
}
setIsSubmitting?.("submitting")
const src = await UploadImageHandler(file, workspaceSlug);
const src = await UploadImageHandler(file);
const { schema } = view.state;
pos = findPlaceholder(view.state, id);
@@ -100,10 +96,7 @@ export async function startImageUpload(file: File, view: EditorView, pos: number
view.dispatch(transaction);
}
const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string> => {
if (!workspaceSlug) {
return Promise.reject("Workspace slug is missing");
}
const UploadImageHandler = (file: File): Promise<string> => {
try {
const formData = new FormData();
formData.append("asset", file);
@@ -111,7 +104,7 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string>
return new Promise(async (resolve, reject) => {
const imageUrl = await fileService
.uploadFile(workspaceSlug, formData)
.uploadFile("plane", formData)
.then((response) => response.asset);
const image = new Image();
+49 -49
View File
@@ -1,56 +1,56 @@
import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image";
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
return {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
},
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
export const TiptapEditorProps: EditorProps = {
attributes: {
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
},
handleDOMEvents: {
keydown: (_view, event) => {
// prevent default event listeners from firing when slash command is active
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
const slashCommand = document.querySelector("#slash-command");
if (slashCommand) {
return true;
}
},
},
handlePaste: (view, event) => {
if (
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files[0]
) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files[0]
) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting);
}
return true;
},
handlePaste: (view, event) => {
if (
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files[0]
) {
event.preventDefault();
const file = event.clipboardData.files[0];
const pos = view.state.selection.from;
startImageUpload(file, view, pos);
return true;
}
return false;
},
handleDrop: (view, event, _slice, moved) => {
if (
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files[0]
) {
event.preventDefault();
const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
// here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1);
}
return false;
},
};
}
return true;
}
return false;
},
};
@@ -52,7 +52,7 @@ const Command = Extension.create({
},
});
const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) =>
const getSuggestionItems = ({ query }: { query: string }) =>
[
{
title: "Text",
@@ -163,7 +163,7 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting);
startImageUpload(file, editor.view, pos);
}
};
input.click();
@@ -264,10 +264,7 @@ const CommandList = ({
>
{items.map((item: CommandItemProps, index: number) => (
<button
className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{ "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex }
)}
className={cn(`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, { "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex })}
key={index}
onClick={() => selectItem(index)}
>
@@ -285,6 +282,8 @@ const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
const container = document.querySelector("#tiptap-container") as HTMLElement;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
component = new ReactRenderer(CommandList, {
@@ -295,7 +294,7 @@ const renderItems = () => {
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector("#tiptap-container"),
appendTo: () => container,
content: component.element,
showOnCreate: true,
interactive: true,
@@ -328,12 +327,11 @@ const renderItems = () => {
};
};
export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
Command.configure({
suggestion: {
items: getSuggestionItems(workspaceSlug, setIsSubmitting),
render: renderItems,
},
});
const SlashCommand = Command.configure({
suggestion: {
items: getSuggestionItems,
render: renderItems,
},
});
export default SlashCommand;
+1 -1
View File
@@ -106,7 +106,7 @@ export const AssigneesList: React.FC<AsigneesListProps> = ({
))}
{users.length > length ? (
<div className="-ml-3.5 relative h-6 w-6 rounded">
<div className="flex items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
<div className="grid place-items-center rounded bg-custom-background-80 text-xs capitalize h-6 w-6 text-custom-text-200 border-[0.5px] border-custom-border-300">
<Icon iconName="add" className="text-xs !leading-3 -mr-0.5" />
{users.length - length}
</div>
-9
View File
@@ -8,13 +8,10 @@ type Props = {
renderAs?: "input" | "button";
value: Date | string | null | undefined;
onChange: (val: string | null) => void;
handleOnOpen?: () => void;
handleOnClose?: () => void;
placeholder?: string;
displayShortForm?: boolean;
error?: boolean;
noBorder?: boolean;
wrapperClassName?: string;
className?: string;
isClearable?: boolean;
disabled?: boolean;
@@ -26,13 +23,10 @@ export const CustomDatePicker: React.FC<Props> = ({
renderAs = "button",
value,
onChange,
handleOnOpen,
handleOnClose,
placeholder = "Select date",
displayShortForm = false,
error = false,
noBorder = false,
wrapperClassName = "",
className = "",
isClearable = true,
disabled = false,
@@ -46,9 +40,6 @@ export const CustomDatePicker: React.FC<Props> = ({
if (!val) onChange(null);
else onChange(renderDateFormat(val));
}}
onCalendarOpen={handleOnOpen}
onCalendarClose={handleOnClose}
wrapperClassName={wrapperClassName}
className={`${
renderAs === "input"
? "block px-2 py-2 text-sm focus:outline-none"
@@ -1,76 +1,47 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect } from "react";
import Link from "next/link";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
type Props = {
clickEvent: React.MouseEvent | null;
position: {
x: number;
y: number;
};
children: React.ReactNode;
title?: string | JSX.Element;
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
const ContextMenu = ({ clickEvent, children, title, isOpen, setIsOpen }: Props) => {
const contextMenuRef = useRef<HTMLDivElement>(null);
// Close the context menu when clicked outside
useOutsideClickDetector(contextMenuRef, () => {
if (isOpen) setIsOpen(false);
});
const ContextMenu = ({ position, children, title, isOpen, setIsOpen }: Props) => {
useEffect(() => {
const hideContextMenu = () => {
if (isOpen) setIsOpen(false);
};
const escapeKeyEvent = (e: KeyboardEvent) => {
if (e.key === "Escape") hideContextMenu();
};
window.addEventListener("click", hideContextMenu);
window.addEventListener("keydown", escapeKeyEvent);
window.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.key === "Escape") hideContextMenu();
});
return () => {
window.removeEventListener("click", hideContextMenu);
window.removeEventListener("keydown", escapeKeyEvent);
window.removeEventListener("keydown", hideContextMenu);
};
}, [isOpen, setIsOpen]);
useEffect(() => {
const contextMenu = contextMenuRef.current;
if (contextMenu && isOpen) {
const contextMenuWidth = contextMenu.clientWidth;
const contextMenuHeight = contextMenu.clientHeight;
const clickX = clickEvent?.pageX || 0;
const clickY = clickEvent?.pageY || 0;
let top = clickY;
// check if there's enough space at the bottom, otherwise show at the top
if (clickY + contextMenuHeight > window.innerHeight) top = clickY - contextMenuHeight;
// check if there's enough space on the right, otherwise show on the left
let left = clickX;
if (clickX + contextMenuWidth > window.innerWidth) left = clickX - contextMenuWidth;
contextMenu.style.top = `${top}px`;
contextMenu.style.left = `${left}px`;
}
}, [clickEvent, isOpen]);
return (
<div
className={`fixed z-50 top-0 left-0 h-full w-full ${
className={`fixed z-20 h-full w-full ${
isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
}`}
>
<div
ref={contextMenuRef}
className={`fixed z-50 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
className={`fixed z-20 flex min-w-[8rem] flex-col items-stretch gap-1 rounded-md border border-custom-border-200 bg-custom-background-90 p-2 text-xs shadow-lg`}
style={{
left: `${position.x}px`,
top: `${position.y}px`,
}}
>
{title && (
<h4 className="border-b border-custom-border-200 px-1 py-1 pb-2 text-[0.8rem] font-medium">
+2 -2
View File
@@ -29,9 +29,9 @@ export const Input: React.FC<Props> = ({
type={type}
id={id}
value={value}
{...(register && register(name ?? "", validations))}
{...(register && register(name, validations))}
onChange={(e) => {
register && register(name ?? "").onChange(e);
register && register(name).onChange(e);
onChange && onChange(e);
}}
className={`block rounded-md bg-transparent text-sm focus:outline-none placeholder-custom-text-400 ${
+1 -1
View File
@@ -3,7 +3,7 @@ import type { UseFormRegister, RegisterOptions } from "react-hook-form";
export interface Props extends React.ComponentPropsWithoutRef<"input"> {
label?: string;
name?: string;
name: string;
value?: string | number | readonly string[];
mode?: "primary" | "transparent" | "trueTransparent" | "secondary" | "disabled";
register?: UseFormRegister<any>;
+12 -14
View File
@@ -1,11 +1,7 @@
import React from "react";
// ui
import { Tooltip } from "components/ui";
// types
import { IIssueLabels } from "types";
type IssueLabelsListProps = {
labels?: (IIssueLabels | undefined)[];
labels?: (string | undefined)[];
length?: number;
showLength?: boolean;
};
@@ -18,16 +14,18 @@ export const IssueLabelsList: React.FC<IssueLabelsListProps> = ({
<>
{labels && (
<>
<Tooltip
position="top"
tooltipHeading="Labels"
tooltipContent={labels.map((l) => l?.name).join(", ")}
>
<div className="flex items-center gap-1.5 px-2 py-1 text-custom-text-200 rounded shadow-sm border border-custom-border-300">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
{`${labels.length} Labels`}
{labels.slice(0, length).map((color, index) => (
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
<span
className={`h-4 w-4 flex-shrink-0 rounded-full border border-custom-border-200
`}
style={{
backgroundColor: color && color !== "" ? color : "#000000",
}}
/>
</div>
</Tooltip>
))}
{labels.length > length ? <span>+{labels.length - length}</span> : null}
</>
)}
</>
@@ -11,8 +11,8 @@ type Props = {
export const ProfileEmptyState: React.FC<Props> = ({ title, description, image }) => (
<div className={`h-full w-full mx-auto grid place-items-center p-8 `}>
<div className="text-center flex flex-col items-center w-full">
<div className="flex items-center justify-center h-14 w-14 rounded-full bg-custom-background-90">
<Image src={image} width={32} alt={title} />
<div className="flex items-center justify-center h-14 w-14 rounded-full bg-custom-primary-10">
<Image src={image} className="w-8" alt={title} />
</div>
<h6 className="text-base font-semibold mt-3.5 mb-3">{title}</h6>
{description && <p className="text-sm text-custom-text-300">{description}</p>}
@@ -45,7 +45,6 @@ const restrictedUrls = [
"profile",
"reset-password",
"sign-up",
"spaces",
"workspace-member-invitation",
];
@@ -1,11 +1,9 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// react-hook-form
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
@@ -28,63 +26,57 @@ type Props = {
user: ICurrentUserResponse | undefined;
};
const defaultValues = {
workspaceName: "",
confirmDelete: "",
};
export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, user }) => {
const router = useRouter();
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [confirmWorkspaceName, setConfirmWorkspaceName] = useState("");
const [confirmDeleteMyWorkspace, setConfirmDeleteMyWorkspace] = useState(false);
const [selectedWorkspace, setSelectedWorkspace] = useState<IWorkspace | null>(null);
const router = useRouter();
const { setToastAlert } = useToast();
const {
control,
formState: { isSubmitting },
handleSubmit,
reset,
watch,
} = useForm({ defaultValues });
useEffect(() => {
if (data) setSelectedWorkspace(data);
else {
const timer = setTimeout(() => {
setSelectedWorkspace(null);
clearTimeout(timer);
}, 350);
}
}, [data]);
const canDelete =
watch("workspaceName") === data?.name && watch("confirmDelete") === "delete my workspace";
const canDelete = confirmWorkspaceName === data?.name && confirmDeleteMyWorkspace;
const handleClose = () => {
const timer = setTimeout(() => {
reset(defaultValues);
clearTimeout(timer);
}, 350);
setIsDeleteLoading(false);
setConfirmWorkspaceName("");
setConfirmDeleteMyWorkspace(false);
onClose();
};
const onSubmit = async () => {
const handleDeletion = async () => {
setIsDeleteLoading(true);
if (!data || !canDelete) return;
await workspaceService
.deleteWorkspace(data.slug, user)
.then(() => {
handleClose();
router.push("/");
mutate<IWorkspace[]>(USER_WORKSPACES, (prevData) =>
prevData?.filter((workspace) => workspace.id !== data.id)
);
setToastAlert({
type: "success",
title: "Success!",
message: "Workspace deleted successfully.",
message: "Workspace deleted successfully",
title: "Success",
});
})
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again later.",
})
);
.catch((error) => {
console.log(error);
setIsDeleteLoading(false);
});
};
return (
@@ -114,7 +106,7 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6 p-6">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<ExclamationTriangleIcon
@@ -139,21 +131,20 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<div className="text-custom-text-200">
<p className="break-words text-sm ">
Enter the workspace name{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span> to
continue:
<span className="font-medium text-custom-text-100">
{selectedWorkspace?.name}
</span>{" "}
to continue:
</p>
<Controller
control={control}
<Input
type="text"
placeholder="Workspace name"
className="mt-2"
value={confirmWorkspaceName}
onChange={(e) => {
setConfirmWorkspaceName(e.target.value);
}}
name="workspaceName"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Workspace name"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
/>
</div>
@@ -163,28 +154,28 @@ export const DeleteWorkspaceModal: React.FC<Props> = ({ isOpen, data, onClose, u
<span className="font-medium text-custom-text-100">delete my workspace</span>{" "}
below:
</p>
<Controller
control={control}
name="confirmDelete"
render={({ field: { onChange, value } }) => (
<Input
type="text"
placeholder="Enter 'delete my workspace'"
className="mt-2"
onChange={onChange}
value={value}
/>
)}
<Input
type="text"
placeholder="Enter 'delete my workspace'"
className="mt-2"
onChange={(e) => {
if (e.target.value === "delete my workspace") {
setConfirmDeleteMyWorkspace(true);
} else {
setConfirmDeleteMyWorkspace(false);
}
}}
name="typeDelete"
/>
</div>
<div className="flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
<DangerButton type="submit" disabled={!canDelete} loading={isSubmitting}>
{isSubmitting ? "Deleting..." : "Delete Workspace"}
<DangerButton onClick={handleDeletion} loading={isDeleteLoading || !canDelete}>
{isDeleteLoading ? "Deleting..." : "Delete Workspace"}
</DangerButton>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
@@ -45,14 +45,12 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
>
<h4 className="capitalize">{type}</h4>
<h4 className="col-span-2">Issue</h4>
<h4>{type === "overdue" ? "Due" : "Start"} Date</h4>
<h4>Due Date</h4>
</div>
<div className="max-h-72 overflow-y-scroll">
{issues.length > 0 ? (
issues.map((issue) => {
const date = type === "overdue" ? issue.target_date : issue.start_date;
const dateDifference = getDateDifference(new Date(date as string));
const dateDifference = getDateDifference(new Date(issue.target_date as string));
return (
<Link
@@ -77,7 +75,7 @@ export const IssuesList: React.FC<Props> = ({ issues, type }) => {
</h5>
<h5 className="col-span-2">{truncateText(issue.name, 30)}</h5>
<h5 className="cursor-default">
{renderShortDateWithYearFormat(new Date(date?.toString() ?? ""))}
{renderShortDateWithYearFormat(new Date(issue.target_date as string))}
</h5>
</div>
</a>
@@ -15,7 +15,7 @@ export const WorkspaceSidebarQuickAction = () => {
}`}
>
<button
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5 ${
className={`flex items-center gap-2 flex-grow rounded flex-shrink-0 py-2 ${
store?.theme?.sidebarCollapsed
? "px-2 hover:bg-custom-sidebar-background-80"
: "px-3 shadow border-[0.5px] border-custom-border-300"
@@ -25,7 +25,7 @@ export const WorkspaceSidebarQuickAction = () => {
document.dispatchEvent(e);
}}
>
<Icon iconName="edit_square" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
<Icon iconName="edit_square" className="!text-xl !leading-5 text-custom-sidebar-text-300" />
{!store?.theme?.sidebarCollapsed && <span className="text-sm font-medium">New Issue</span>}
</button>
@@ -40,7 +40,7 @@ export const WorkspaceSidebarQuickAction = () => {
document.dispatchEvent(e);
}}
>
<Icon iconName="search" className="!text-lg !leading-4 text-custom-sidebar-text-300" />
<Icon iconName="search" className="!text-xl !leading-5 text-custom-sidebar-text-300" />
</button>
</div>
);
+4 -4
View File
@@ -42,10 +42,10 @@ export const ANALYTICS_X_AXIS_VALUES: { value: TXAxisValues; label: string }[] =
value: "target_date",
label: "Due date",
},
{
value: "start_date",
label: "Start date",
},
// {
// value: "start_date",
// label: "Start date",
// },
{
value: "created_at",
label: "Created date",
+3 -3
View File
@@ -1,9 +1,9 @@
// services images
import GithubLogo from "public/services/github.png";
import JiraLogo from "public/services/jira.png";
import CSVLogo from "public/services/csv.svg";
import ExcelLogo from "public/services/excel.svg";
import JSONLogo from "public/services/json.svg";
import CSVLogo from "public/services/csv.png";
import ExcelLogo from "public/services/excel.png";
import JSONLogo from "public/services/json.png";
export const ROLE = {
5: "Guest",
+2 -2
View File
@@ -122,7 +122,7 @@ export const InboxViewContextProvider: React.FC<{ children: React.ReactNode }> =
},
};
mutateInboxDetails((prevData: any) => {
mutateInboxDetails((prevData) => {
if (!prevData) return prevData;
return {
@@ -156,7 +156,7 @@ export const InboxViewContextProvider: React.FC<{ children: React.ReactNode }> =
filters: { ...initialState.filters },
};
mutateInboxDetails((prevData: any) => {
mutateInboxDetails((prevData) => {
if (!prevData) return prevData;
return {
+8 -24
View File
@@ -362,13 +362,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
},
});
const additionalProperties = {
groupByProperty: state.groupByProperty,
orderBy: state.orderBy,
};
if (property === "kanban" && state.groupByProperty === null) {
additionalProperties.groupByProperty = "state";
if (property === "kanban") {
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
@@ -377,7 +371,6 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
});
}
if (property === "calendar") {
additionalProperties.groupByProperty = null;
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
@@ -385,22 +378,13 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
},
});
}
if (property === "gantt_chart") {
additionalProperties.orderBy = "sort_order";
dispatch({
type: "SET_ORDER_BY_PROPERTY",
payload: {
orderBy: "sort_order",
},
});
}
if (!workspaceSlug || !projectId) return;
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
issueView: property,
...additionalProperties,
groupByProperty: "state",
});
},
[workspaceSlug, projectId, state]
@@ -417,7 +401,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -448,7 +432,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -479,7 +463,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -510,7 +494,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -541,7 +525,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -663,7 +647,7 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
user
);
} else {
mutateMyViewProps((prevData: any) => {
mutateMyViewProps((prevData) => {
if (!prevData) return prevData;
return {
@@ -120,7 +120,7 @@ const UserNotificationContextProvider: React.FC<{
const handleReadMutation = (action: "read" | "unread") => {
const notificationCountNumber = action === "read" ? -1 : 1;
mutateNotificationCount((prev: any) => {
mutateNotificationCount((prev) => {
if (!prev) return prev;
const notificationType: keyof NotificationCount =
@@ -143,8 +143,8 @@ const UserNotificationContextProvider: React.FC<{
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
notificationsMutate(
(previousNotifications: any) =>
previousNotifications?.map((notification: any) =>
(previousNotifications) =>
previousNotifications?.map((notification) =>
notification.id === notificationId
? { ...notification, read_at: isRead ? null : new Date() }
: notification
@@ -199,8 +199,7 @@ const UserNotificationContextProvider: React.FC<{
});
} else {
notificationsMutate(
(prev: any) =>
prev?.filter((prevNotification: any) => prevNotification.id !== notificationId),
(prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId),
false
);
await userNotificationServices
@@ -223,8 +222,8 @@ const UserNotificationContextProvider: React.FC<{
null;
notificationsMutate(
(previousNotifications: any) =>
previousNotifications?.map((notification: any) =>
(previousNotifications) =>
previousNotifications?.map((notification) =>
notification.id === notificationId
? { ...notification, snoozed_till: isSnoozed ? null : new Date(dateTime!) }
: notification
+3 -3
View File
@@ -56,7 +56,7 @@ const useCommentReaction = (
user.user
);
mutateCommentReactions((prev: any) => [...(prev || []), data]);
mutateCommentReactions((prev) => [...(prev || []), data]);
};
/**
@@ -69,8 +69,8 @@ const useCommentReaction = (
if (!workspaceSlug || !projectId || !commendId) return;
mutateCommentReactions(
(prevData: any) =>
prevData?.filter((r: any) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
(prevData) =>
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
false
);
+1 -1
View File
@@ -64,7 +64,7 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
if (issueProperties && projectId) {
mutateIssueProperties(
(prev: any) =>
(prev) =>
({
...prev,
properties: { ...prev?.properties, [key]: !prev?.properties?.[key] },
+3 -3
View File
@@ -56,7 +56,7 @@ const useIssueReaction = (
user.user
);
mutateReaction((prev: any) => [...(prev || []), data]);
mutateReaction((prev) => [...(prev || []), data]);
};
/**
@@ -69,8 +69,8 @@ const useIssueReaction = (
if (!workspaceSlug || !projectId || !issueId) return;
mutateReaction(
(prevData: any) =>
prevData?.filter((r: any) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
(prevData) =>
prevData?.filter((r) => r.actor !== user?.user?.id || r.reaction !== reaction) || [],
false
);
@@ -8,10 +8,10 @@ const useOutsideClickDetector = (ref: React.RefObject<HTMLElement>, callback: ()
};
useEffect(() => {
document.addEventListener("mousedown", handleClick);
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
document.removeEventListener("click", handleClick);
};
});
};
+9 -9
View File
@@ -75,7 +75,7 @@ const useUserNotification = () => {
const handleReadMutation = (action: "read" | "unread") => {
const notificationCountNumber = action === "read" ? -1 : 1;
mutateNotificationCount((prev: any) => {
mutateNotificationCount((prev) => {
if (!prev) return prev;
const notificationType: keyof NotificationCount =
@@ -93,18 +93,18 @@ const useUserNotification = () => {
};
const mutateNotification = (notificationId: string, value: Object) => {
notificationMutate((previousNotifications: any) => {
notificationMutate((previousNotifications) => {
if (!previousNotifications) return previousNotifications;
const notificationIndex = Math.floor(
previousNotifications
.map((d: any) => d.results)
.map((d) => d.results)
.flat()
.findIndex((notification: any) => notification.id === notificationId) / PER_PAGE
.findIndex((notification) => notification.id === notificationId) / PER_PAGE
);
let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex(
(notification: any) => notification.id === notificationId
(notification) => notification.id === notificationId
);
if (notificationIndexInPage === -1) return previousNotifications;
@@ -126,18 +126,18 @@ const useUserNotification = () => {
};
const removeNotification = (notificationId: string) => {
notificationMutate((previousNotifications: any) => {
notificationMutate((previousNotifications) => {
if (!previousNotifications) return previousNotifications;
const notificationIndex = Math.floor(
previousNotifications
.map((d: any) => d.results)
.map((d) => d.results)
.flat()
.findIndex((notification: any) => notification.id === notificationId) / PER_PAGE
.findIndex((notification) => notification.id === notificationId) / PER_PAGE
);
let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex(
(notification: any) => notification.id === notificationId
(notification) => notification.id === notificationId
);
if (notificationIndexInPage === -1) return previousNotifications;
+20 -61
View File
@@ -1,12 +1,5 @@
// next imports
import { useRouter } from "next/router";
import Link from "next/link";
// icons
import { Bars3Icon } from "@heroicons/react/24/outline";
// ui components
import { Tooltip } from "components/ui";
// hooks
import useProjectDetails from "hooks/use-project-details";
type Props = {
breadcrumbs?: JSX.Element;
@@ -16,61 +9,27 @@ type Props = {
noHeader: boolean;
};
const { NEXT_PUBLIC_DEPLOY_URL } = process.env;
const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL ? NEXT_PUBLIC_DEPLOY_URL : "http://localhost:3001";
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => {
const { projectDetails } = useProjectDetails();
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
return (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 px-5 py-4 ${
noHeader ? "md:hidden" : ""
}`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => setToggleSidebar((prevData) => !prevData)}
>
<Bars3Icon className="h-5 w-5" />
</button>
</div>
<div>{breadcrumbs}</div>
{projectDetails && projectDetails?.is_deployed && (
<Link href={`${plane_deploy_url}/${workspaceSlug}/${projectId}`}>
<a target="_blank" rel="noreferrer">
<Tooltip
tooltipContent="This project is public, and live on web."
position="bottom-left"
>
<div className="transition-all flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 p-1 rounded overflow-hidden relative flex items-center gap-1 cursor-pointer group">
<div className="w-[14px] h-[14px] flex justify-center items-center">
<span className="material-symbols-rounded text-[14px]">
radio_button_checked
</span>
</div>
<div className="text-xs font-medium">Public</div>
<div className="w-[14px] h-[14px] hidden group-hover:flex justify-center items-center">
<span className="material-symbols-rounded text-[14px]">open_in_new</span>
</div>
</div>
</Tooltip>
</a>
</Link>
)}
<div className="flex-shrink-0">{left}</div>
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => (
<div
className={`relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 px-5 py-4 ${
noHeader ? "md:hidden" : ""
}`}
>
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => setToggleSidebar((prevData) => !prevData)}
>
<Bars3Icon className="h-5 w-5" />
</button>
</div>
<div className="flex-shrink-0">{right}</div>
{breadcrumbs}
<div className="flex-shrink-0">{left}</div>
</div>
);
};
<div className="flex-shrink-0">{right}</div>
</div>
);
export default Header;
@@ -8,7 +8,6 @@ import {
WorkspaceSidebarQuickAction,
} from "components/workspace";
import { ProjectSidebarList } from "components/project";
import { PublishProjectModal } from "components/project/publish-project/modal";
// mobx react lite
import { observer } from "mobx-react-lite";
// mobx store
@@ -38,7 +37,6 @@ const Sidebar: React.FC<SidebarProps> = observer(({ toggleSidebar, setToggleSide
<ProjectSidebarList />
<WorkspaceHelpSection setSidebarActive={setToggleSidebar} />
</div>
<PublishProjectModal />
</div>
);
});

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