Compare commits

..

1 Commits

Author SHA1 Message Date
NarayanBavisetti e1145a7d38 chore: issue patch external api 2024-04-24 18:07:23 +05:30
329 changed files with 5238 additions and 4990 deletions
+84
View File
@@ -0,0 +1,84 @@
name: Auto Merge or Create PR on Push
on:
workflow_dispatch:
push:
branches:
- "sync/**"
env:
CURRENT_BRANCH: ${{ github.ref_name }}
SOURCE_BRANCH: ${{ secrets.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce"
TARGET_BRANCH: ${{ secrets.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
REVIEWER: ${{ secrets.SYNC_PR_REVIEWER }}
jobs:
Check_Branch:
runs-on: ubuntu-latest
outputs:
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
steps:
- name: Check if current branch matches the secret
id: check-branch
run: |
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
echo "MATCH=true" >> $GITHUB_OUTPUT
else
echo "MATCH=false" >> $GITHUB_OUTPUT
fi
Auto_Merge:
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
needs: [Check_Branch]
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Setup Git
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
- name: Setup GH CLI and Git Config
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Check for merge conflicts
id: conflicts
run: |
git fetch origin $TARGET_BRANCH
git checkout $TARGET_BRANCH
# Attempt to merge the main branch into the current branch
if $(git merge --no-commit --no-ff $SOURCE_BRANCH); then
echo "No merge conflicts detected."
echo "HAS_CONFLICTS=false" >> $GITHUB_ENV
else
echo "Merge conflicts detected."
echo "HAS_CONFLICTS=true" >> $GITHUB_ENV
git merge --abort
fi
- name: Merge Change to Target Branch
if: env.HAS_CONFLICTS == 'false'
run: |
git commit -m "Merge branch '$SOURCE_BRANCH' into $TARGET_BRANCH"
git push origin $TARGET_BRANCH
- name: Create PR to Target Branch
if: env.HAS_CONFLICTS == 'true'
run: |
# Replace 'username' with the actual GitHub username of the reviewer.
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "" --reviewer $REVIEWER)
echo "Pull Request created: $PR_URL"
+30 -44
View File
@@ -1,53 +1,28 @@
name: Create PR on Sync
name: Create Sync Action
on:
workflow_dispatch:
push:
branches:
- "sync/**"
- preview
env:
CURRENT_BRANCH: ${{ github.ref_name }}
SOURCE_BRANCH: ${{ vars.SYNC_SOURCE_BRANCH_NAME }} # The sync branch such as "sync/ce"
TARGET_BRANCH: ${{ vars.SYNC_TARGET_BRANCH_NAME }} # The target branch that you would like to merge changes like develop
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
REVIEWER: ${{ vars.SYNC_PR_REVIEWER }}
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
jobs:
Check_Branch:
runs-on: ubuntu-latest
outputs:
BRANCH_MATCH: ${{ steps.check-branch.outputs.MATCH }}
steps:
- name: Check if current branch matches the secret
id: check-branch
run: |
if [ "$CURRENT_BRANCH" = "$SOURCE_BRANCH" ]; then
echo "MATCH=true" >> $GITHUB_OUTPUT
else
echo "MATCH=false" >> $GITHUB_OUTPUT
fi
Auto_Merge:
if: ${{ needs.Check_Branch.outputs.BRANCH_MATCH == 'true' }}
needs: [Check_Branch]
sync_changes:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
contents: read
steps:
- name: Checkout code
- name: Checkout Code
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
persist-credentials: false
fetch-depth: 0
- name: Setup Git
run: |
git config user.name "$ACCOUNT_USER_NAME"
git config user.email "$ACCOUNT_USER_EMAIL"
- name: Setup GH CLI and Git Config
- name: Setup GH CLI
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
@@ -56,14 +31,25 @@ jobs:
sudo apt update
sudo apt install gh -y
- name: Create PR to Target Branch
- name: Push Changes to Target Repo A
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
# get all pull requests and check if there is already a PR
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $SOURCE_BRANCH --state open --json number | jq '.[] | .number')
if [ -n "$PR_EXISTS" ]; then
echo "Pull Request already exists: $PR_EXISTS"
else
echo "Creating new pull request"
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $SOURCE_BRANCH --title "sync: merge conflicts need to be resolved" --body "")
echo "Pull Request created: $PR_URL"
fi
TARGET_REPO="${{ secrets.TARGET_REPO_A }}"
TARGET_BRANCH="${{ secrets.TARGET_REPO_A_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
- name: Push Changes to Target Repo B
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ secrets.TARGET_REPO_B }}"
TARGET_BRANCH="${{ secrets.TARGET_REPO_B_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git remote add target-origin-b "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin-b $SOURCE_BRANCH:$TARGET_BRANCH
-44
View File
@@ -1,44 +0,0 @@
name: Sync Repositories
on:
workflow_dispatch:
push:
branches:
- preview
env:
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
jobs:
sync_changes:
runs-on: ubuntu-20.04
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4.1.1
with:
persist-credentials: false
fetch-depth: 0
- name: Setup GH CLI
run: |
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh -y
- name: Push Changes to Target Repo
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
-6
View File
@@ -6,15 +6,9 @@ from plane.api.views import (
IssueLinkAPIEndpoint,
IssueCommentAPIEndpoint,
IssueActivityAPIEndpoint,
WorkspaceIssueAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/issues/<str:project__identifier>-<str:issue__identifier>/",
WorkspaceIssueAPIEndpoint.as_view(),
name="issue-by-identifier",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueAPIEndpoint.as_view(),
-1
View File
@@ -3,7 +3,6 @@ from .project import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
from .state import StateAPIEndpoint
from .issue import (
WorkspaceIssueAPIEndpoint,
IssueAPIEndpoint,
LabelAPIEndpoint,
IssueLinkAPIEndpoint,
+1 -1
View File
@@ -515,7 +515,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueAPIEndpoint(BaseAPIView):
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`,
and `destroy` actions related to cycle issues.
+4 -66
View File
@@ -48,72 +48,10 @@ from plane.db.models import (
ProjectMember,
)
from .base import BaseAPIView
from .base import BaseAPIView, WebhookMixin
class WorkspaceIssueAPIEndpoint(BaseAPIView):
"""
This viewset provides `retrieveByIssueId` on workspace level
"""
model = Issue
webhook_event = "issue"
permission_classes = [ProjectEntityPermission]
serializer_class = IssueSerializer
@property
def project__identifier(self):
return self.kwargs.get("project__identifier", None)
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__identifier=self.kwargs.get("project__identifier"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(
workspace__slug=slug,
project__identifier=project__identifier,
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
class IssueAPIEndpoint(BaseAPIView):
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to issue.
@@ -499,7 +437,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer = LabelSerializer(label, data=request.data, partial=True)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
request.data.get("external_id")
and (label.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
@@ -655,7 +593,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssueCommentAPIEndpoint(BaseAPIView):
class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to comments of the particular issue.
+1 -1
View File
@@ -260,7 +260,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to module issues.
@@ -79,16 +79,6 @@ class ProjectEntityPermission(BasePermission):
if request.user.is_anonymous:
return False
# Handle requests based on project__identifier
if hasattr(view, "project__identifier") and view.project__identifier:
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project__identifier=view.project__identifier,
is_active=True,
).exists()
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
+3 -4
View File
@@ -23,7 +23,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
IssueSerializer,
CycleIssueSerializer,
@@ -40,7 +40,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
class CycleIssueViewSet(BaseViewSet):
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleIssueSerializer
model = CycleIssue
@@ -249,7 +249,6 @@ class CycleIssueViewSet(BaseViewSet):
update_cycle_issue_activity = []
# Iterate over each cycle_issue in cycle_issues
for cycle_issue in cycle_issues:
old_cycle_id = cycle_issue.cycle_id
# Update the cycle_issue's cycle_id
cycle_issue.cycle_id = cycle_id
# Add the modified cycle_issue to the records_to_update list
@@ -257,7 +256,7 @@ class CycleIssueViewSet(BaseViewSet):
# Record the update activity
update_cycle_issue_activity.append(
{
"old_cycle_id": str(old_cycle_id),
"old_cycle_id": str(cycle_issue.cycle_id),
"new_cycle_id": str(cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
+2 -2
View File
@@ -52,7 +52,7 @@ from plane.db.models import (
from plane.utils.issue_filters import issue_filters
# Module imports
from .. import BaseAPIView, BaseViewSet
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class IssueListEndpoint(BaseAPIView):
@@ -244,7 +244,7 @@ class IssueListEndpoint(BaseAPIView):
return Response(issues, status=status.HTTP_200_OK)
class IssueViewSet(BaseViewSet):
class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
return (
IssueCreateSerializer
+2 -2
View File
@@ -11,7 +11,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
IssueCommentSerializer,
CommentReactionSerializer,
@@ -25,7 +25,7 @@ from plane.db.models import (
from plane.bgtasks.issue_activites_task import issue_activity
class IssueCommentViewSet(BaseViewSet):
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
+2 -2
View File
@@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
ModuleIssueSerializer,
IssueSerializer,
@@ -33,7 +33,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
class ModuleIssueViewSet(BaseViewSet):
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = ModuleIssueSerializer
model = ModuleIssue
webhook_event = "module_issue"
+2 -2
View File
@@ -151,8 +151,8 @@ class WorkSpaceViewSet(BaseViewSet):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -21,7 +21,6 @@ class WorkspaceStatesEndpoint(BaseAPIView):
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
is_triage=False,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
@@ -31,7 +31,6 @@ from plane.db.models import (
)
from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception
from plane.bgtasks.webhook_task import webhook_activity
# Track Changes in name
@@ -1693,19 +1692,6 @@ def issue_activity(
except Exception as e:
log_exception(e)
for activity in issue_activities_created:
webhook_activity.delay(
event="issue",
event_id=activity.issue_id,
verb=activity.verb,
field=activity.field,
old_value=activity.old_value,
new_value=activity.new_value,
actor_id=activity.actor_id,
current_site=origin,
slug=activity.workspace.slug,
)
if notification:
notifications.delay(
type=type,
-166
View File
@@ -294,169 +294,3 @@ def send_webhook_deactivation_email(
except Exception as e:
log_exception(e)
return
@shared_task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=600,
max_retries=5,
retry_jitter=True,
)
def webhook_send_task(
self,
webhook,
slug,
event,
event_data,
action,
current_site,
activity,
):
try:
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
headers = {
"Content-Type": "application/json",
"User-Agent": "Autopilot",
"X-Plane-Delivery": str(uuid.uuid4()),
"X-Plane-Event": event,
}
# # Your secret key
event_data = (
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
if event_data is not None
else None
)
action = {
"POST": "create",
"PATCH": "update",
"PUT": "update",
"DELETE": "delete",
}.get(action, action)
payload = {
"event": event,
"action": action,
"webhook_id": str(webhook.id),
"workspace_id": str(webhook.workspace_id),
"data": event_data,
"activity": activity,
}
# Use HMAC for generating signature
if webhook.secret_key:
hmac_signature = hmac.new(
webhook.secret_key.encode("utf-8"),
json.dumps(payload).encode("utf-8"),
hashlib.sha256,
)
signature = hmac_signature.hexdigest()
headers["X-Plane-Signature"] = signature
# Send the webhook event
response = requests.post(
webhook.url,
headers=headers,
json=payload,
timeout=30,
)
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=str(response.status_code),
response_headers=str(response.headers),
response_body=str(response.text),
retry_count=str(self.request.retries),
)
except requests.RequestException as e:
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=500,
response_headers="",
response_body=str(e),
retry_count=str(self.request.retries),
)
# Retry logic
if self.request.retries >= self.max_retries:
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
if webhook:
# send email for the deactivation of the webhook
send_webhook_deactivation_email(
webhook_id=webhook.id,
receiver_id=webhook.created_by_id,
reason=str(e),
current_site=current_site,
)
return
raise requests.RequestException()
except Exception as e:
if settings.DEBUG:
print(e)
log_exception(e)
return
@shared_task
def webhook_activity(
event,
verb,
field,
old_value,
new_value,
actor_id,
slug,
current_site,
event_id,
):
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
if event == "project":
webhooks = webhooks.filter(project=True)
if event == "issue":
webhooks = webhooks.filter(issue=True)
if event == "module" or event == "module_issue":
webhooks = webhooks.filter(module=True)
if event == "cycle" or event == "cycle_issue":
webhooks = webhooks.filter(cycle=True)
if event == "issue_comment":
webhooks = webhooks.filter(issue_comment=True)
for webhook in webhooks:
webhook_send_task.delay(
webhook=webhook.id,
slug=slug,
event=event,
event_data=get_model_data(
event=event,
event_id=event_id,
),
action=verb,
current_site=current_site,
activity={
"field": field,
"new_value": new_value,
"old_value": old_value,
"actor_id": actor_id,
},
)
+1 -1
View File
@@ -180,7 +180,7 @@ services:
plane-redis:
container_name: plane-redis
image: redis:7.2.4-alpine
image: redis:6.2.7-alpine
restart: always
volumes:
- redisdata:/data
+1 -1
View File
@@ -10,7 +10,7 @@ volumes:
services:
plane-redis:
image: redis:7.2.4-alpine
image: redis:6.2.7-alpine
restart: unless-stopped
networks:
- dev_env
+1 -1
View File
@@ -90,7 +90,7 @@ services:
plane-redis:
container_name: plane-redis
image: redis:7.2.4-alpine
image: redis:6.2.7-alpine
restart: always
volumes:
- redisdata:/data
@@ -34,7 +34,7 @@ interface CustomEditorProps {
suggestions?: () => Promise<IMentionSuggestion[]>;
};
handleEditorReady?: (value: boolean) => void;
placeholder?: string | ((isFocused: boolean, value: string) => string);
placeholder?: string | ((isFocused: boolean) => string);
tabIndex?: number;
}
@@ -142,11 +142,11 @@ export const useEditor = ({
executeMenuItemCommand: (itemName: EditorMenuItemNames) => {
const editorItems = getEditorMenuItems(editorRef.current, uploadFile);
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.name === itemName);
const item = getEditorMenuItem(itemName);
if (item) {
if (item.key === "image") {
if (item.name === "image") {
item.command(savedSelection);
} else {
item.command();
@@ -158,7 +158,7 @@ export const useEditor = ({
isMenuItemActive: (itemName: EditorMenuItemNames): boolean => {
const editorItems = getEditorMenuItems(editorRef.current, uploadFile);
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.name === itemName);
const item = getEditorMenuItem(itemName);
return item ? item.isActive() : false;
},
@@ -4,11 +4,6 @@ import { findTableAncestor } from "src/lib/utils";
import { Selection } from "@tiptap/pm/state";
import { UploadImage } from "src/types/upload-image";
export const setText = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).clearNodes().run();
else editor.chain().focus().clearNodes().run();
};
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
else editor.chain().focus().toggleHeading({ level: 1 }).run();
@@ -24,21 +19,6 @@ export const toggleHeadingThree = (editor: Editor, range?: Range) => {
else editor.chain().focus().toggleHeading({ level: 3 }).run();
};
export const toggleHeadingFour = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run();
else editor.chain().focus().toggleHeading({ level: 4 }).run();
};
export const toggleHeadingFive = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run();
else editor.chain().focus().toggleHeading({ level: 5 }).run();
};
export const toggleHeadingSix = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run();
else editor.chain().focus().toggleHeading({ level: 6 }).run();
};
export const toggleBold = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).toggleBold().run();
else editor.chain().focus().toggleBold().run();
@@ -330,29 +330,6 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
line-height: 1.3;
}
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: 1rem;
line-height: 1.5;
}
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: 0.9rem;
font-weight: 600;
line-height: 1.5;
}
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 1rem;
margin-bottom: 1px;
font-size: 0.83rem;
font-weight: 600;
line-height: 1.5;
}
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
margin-top: 0.25rem;
margin-bottom: 1px;
@@ -43,7 +43,7 @@ type TArguments = {
cancelUploadImage?: () => void;
uploadFile: UploadImage;
};
placeholder?: string | ((isFocused: boolean, value: string) => string);
placeholder?: string | ((isFocused: boolean) => string);
tabIndex?: number;
};
@@ -147,7 +147,7 @@ export const CoreEditorExtensions = ({
if (placeholder) {
if (typeof placeholder === "string") return placeholder;
else return placeholder(editor.isFocused, editor.getHTML());
else return placeholder(editor.isFocused);
}
return "Press '/' for commands...";
@@ -13,24 +13,16 @@ import {
UnderlineIcon,
StrikethroughIcon,
CodeIcon,
Heading4,
Heading5,
Heading6,
CaseSensitive,
} from "lucide-react";
import { Editor } from "@tiptap/react";
import {
insertImageCommand,
insertTableCommand,
setText,
toggleBlockquote,
toggleBold,
toggleBulletList,
toggleCodeBlock,
toggleHeadingFive,
toggleHeadingFour,
toggleHeadingOne,
toggleHeadingSix,
toggleHeadingThree,
toggleHeadingTwo,
toggleItalic,
@@ -44,26 +36,15 @@ import { UploadImage } from "src/types/upload-image";
import { Selection } from "@tiptap/pm/state";
export interface EditorMenuItem {
key: string;
name: string;
isActive: () => boolean;
command: () => void;
icon: LucideIconType;
}
export const TextItem = (editor: Editor) =>
({
key: "text",
name: "Text",
isActive: () => editor.isActive("paragraph"),
command: () => setText(editor),
icon: CaseSensitive,
}) as const satisfies EditorMenuItem;
export const HeadingOneItem = (editor: Editor) =>
({
key: "h1",
name: "Heading 1",
name: "H1",
isActive: () => editor.isActive("heading", { level: 1 }),
command: () => toggleHeadingOne(editor),
icon: Heading1,
@@ -71,8 +52,7 @@ export const HeadingOneItem = (editor: Editor) =>
export const HeadingTwoItem = (editor: Editor) =>
({
key: "h2",
name: "Heading 2",
name: "H2",
isActive: () => editor.isActive("heading", { level: 2 }),
command: () => toggleHeadingTwo(editor),
icon: Heading2,
@@ -80,44 +60,15 @@ export const HeadingTwoItem = (editor: Editor) =>
export const HeadingThreeItem = (editor: Editor) =>
({
key: "h3",
name: "Heading 3",
name: "H3",
isActive: () => editor.isActive("heading", { level: 3 }),
command: () => toggleHeadingThree(editor),
icon: Heading3,
}) as const satisfies EditorMenuItem;
export const HeadingFourItem = (editor: Editor) =>
({
key: "h4",
name: "Heading 4",
isActive: () => editor.isActive("heading", { level: 4 }),
command: () => toggleHeadingFour(editor),
icon: Heading4,
}) as const satisfies EditorMenuItem;
export const HeadingFiveItem = (editor: Editor) =>
({
key: "h5",
name: "Heading 5",
isActive: () => editor.isActive("heading", { level: 5 }),
command: () => toggleHeadingFive(editor),
icon: Heading5,
}) as const satisfies EditorMenuItem;
export const HeadingSixItem = (editor: Editor) =>
({
key: "h6",
name: "Heading 6",
isActive: () => editor.isActive("heading", { level: 6 }),
command: () => toggleHeadingSix(editor),
icon: Heading6,
}) as const satisfies EditorMenuItem;
export const BoldItem = (editor: Editor) =>
({
key: "bold",
name: "Bold",
name: "bold",
isActive: () => editor?.isActive("bold"),
command: () => toggleBold(editor),
icon: BoldIcon,
@@ -125,8 +76,7 @@ export const BoldItem = (editor: Editor) =>
export const ItalicItem = (editor: Editor) =>
({
key: "italic",
name: "Italic",
name: "italic",
isActive: () => editor?.isActive("italic"),
command: () => toggleItalic(editor),
icon: ItalicIcon,
@@ -134,8 +84,7 @@ export const ItalicItem = (editor: Editor) =>
export const UnderLineItem = (editor: Editor) =>
({
key: "underline",
name: "Underline",
name: "underline",
isActive: () => editor?.isActive("underline"),
command: () => toggleUnderline(editor),
icon: UnderlineIcon,
@@ -143,8 +92,7 @@ export const UnderLineItem = (editor: Editor) =>
export const StrikeThroughItem = (editor: Editor) =>
({
key: "strikethrough",
name: "Strikethrough",
name: "strike",
isActive: () => editor?.isActive("strike"),
command: () => toggleStrike(editor),
icon: StrikethroughIcon,
@@ -152,53 +100,47 @@ export const StrikeThroughItem = (editor: Editor) =>
export const BulletListItem = (editor: Editor) =>
({
key: "bulleted-list",
name: "Bulleted list",
name: "bullet-list",
isActive: () => editor?.isActive("bulletList"),
command: () => toggleBulletList(editor),
icon: ListIcon,
}) as const satisfies EditorMenuItem;
export const NumberedListItem = (editor: Editor) =>
({
key: "numbered-list",
name: "Numbered list",
isActive: () => editor?.isActive("orderedList"),
command: () => toggleOrderedList(editor),
icon: ListOrderedIcon,
}) as const satisfies EditorMenuItem;
export const TodoListItem = (editor: Editor) =>
({
key: "to-do-list",
name: "To-do list",
name: "To-do List",
isActive: () => editor.isActive("taskItem"),
command: () => toggleTaskList(editor),
icon: CheckSquare,
}) as const satisfies EditorMenuItem;
export const QuoteItem = (editor: Editor) =>
({
key: "quote",
name: "Quote",
isActive: () => editor?.isActive("blockquote"),
command: () => toggleBlockquote(editor),
icon: QuoteIcon,
}) as const satisfies EditorMenuItem;
export const CodeItem = (editor: Editor) =>
({
key: "code",
name: "Code",
name: "code",
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
command: () => toggleCodeBlock(editor),
icon: CodeIcon,
}) as const satisfies EditorMenuItem;
export const NumberedListItem = (editor: Editor) =>
({
name: "ordered-list",
isActive: () => editor?.isActive("orderedList"),
command: () => toggleOrderedList(editor),
icon: ListOrderedIcon,
}) as const satisfies EditorMenuItem;
export const QuoteItem = (editor: Editor) =>
({
name: "quote",
isActive: () => editor?.isActive("blockquote"),
command: () => toggleBlockquote(editor),
icon: QuoteIcon,
}) as const satisfies EditorMenuItem;
export const TableItem = (editor: Editor) =>
({
key: "table",
name: "Table",
name: "table",
isActive: () => editor?.isActive("table"),
command: () => insertTableCommand(editor),
icon: TableIcon,
@@ -206,8 +148,7 @@ export const TableItem = (editor: Editor) =>
export const ImageItem = (editor: Editor, uploadFile: UploadImage) =>
({
key: "image",
name: "Image",
name: "image",
isActive: () => editor?.isActive("image"),
command: (savedSelection: Selection | null) => insertImageCommand(editor, uploadFile, savedSelection),
icon: ImageIcon,
@@ -218,13 +159,9 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
return [];
}
return [
TextItem(editor),
HeadingOneItem(editor),
HeadingTwoItem(editor),
HeadingThreeItem(editor),
HeadingFourItem(editor),
HeadingFiveItem(editor),
HeadingSixItem(editor),
BoldItem(editor),
ItalicItem(editor),
UnderLineItem(editor),
@@ -240,7 +177,7 @@ export function getEditorMenuItems(editor: Editor | null, uploadFile: UploadImag
}
export type EditorMenuItemNames = ReturnType<typeof getEditorMenuItems> extends (infer U)[]
? U extends { key: infer N }
? U extends { name: infer N }
? N
: never
: never;
@@ -31,7 +31,7 @@ interface IDocumentEditor {
suggestions: () => Promise<IMentionSuggestion[]>;
};
tabIndex?: number;
placeholder?: string | ((isFocused: boolean, value: string) => string);
placeholder?: string | ((isFocused: boolean) => string);
}
const DocumentEditor = (props: IDocumentEditor) => {
@@ -67,13 +67,11 @@ export default function BlockMenu(props: BlockMenuProps) {
popup.current?.hide();
};
document.addEventListener("click", handleClickDragHandle);
document.addEventListener("contextmenu", handleClickDragHandle);
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("scroll", handleScroll, true); // Using capture phase
return () => {
document.removeEventListener("click", handleClickDragHandle);
document.removeEventListener("contextmenu", handleClickDragHandle);
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("scroll", handleScroll, true);
};
@@ -225,9 +225,6 @@ function DragHandle(options: DragHandleOptions) {
dragHandleElement.addEventListener("click", (e) => {
handleClick(e, view);
});
dragHandleElement.addEventListener("contextmenu", (e) => {
handleClick(e, view);
});
dragHandleElement.addEventListener("drag", (e) => {
hideDragHandle();
@@ -32,7 +32,7 @@ export interface ILiteTextEditor {
suggestions?: () => Promise<IMentionSuggestion[]>;
};
tabIndex?: number;
placeholder?: string | ((isFocused: boolean, value: string) => string);
placeholder?: string | ((isFocused: boolean) => string);
}
const LiteTextEditor = (props: ILiteTextEditor) => {
@@ -35,7 +35,7 @@ export type IRichTextEditor = {
highlights: () => Promise<IMentionHighlight[]>;
suggestions: () => Promise<IMentionSuggestion[]>;
};
placeholder?: string | ((isFocused: boolean, value: string) => string);
placeholder?: string | ((isFocused: boolean) => string);
tabIndex?: number;
};
@@ -15,7 +15,6 @@ import {
} from "@plane/editor-core";
export interface BubbleMenuItem {
key: string;
name: string;
isActive: () => boolean;
command: () => void;
@@ -8,13 +8,9 @@ import {
QuoteItem,
CodeItem,
TodoListItem,
TextItem,
HeadingFourItem,
HeadingFiveItem,
HeadingSixItem,
} from "@plane/editor-core";
import { Editor } from "@tiptap/react";
import { Check, ChevronDown } from "lucide-react";
import { Check, ChevronDown, TextIcon } from "lucide-react";
import { Dispatch, FC, SetStateAction } from "react";
import { BubbleMenuItem } from ".";
@@ -27,16 +23,18 @@ interface NodeSelectorProps {
export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
const items: BubbleMenuItem[] = [
TextItem(editor),
{
name: "Text",
icon: TextIcon,
command: () => editor.chain().focus().clearNodes().run(),
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
},
HeadingOneItem(editor),
HeadingTwoItem(editor),
HeadingThreeItem(editor),
HeadingFourItem(editor),
HeadingFiveItem(editor),
HeadingSixItem(editor),
TodoListItem(editor),
BulletListItem(editor),
NumberedListItem(editor),
TodoListItem(editor),
QuoteItem(editor),
CodeItem(editor),
];
@@ -60,7 +58,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
</button>
{isOpen && (
<section className="fixed top-full z-[99999] mt-1 flex w-48 flex-col overflow-hidden rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg animate-in fade-in slide-in-from-top-1">
<section className="fixed top-full z-[99999] mt-1 flex w-48 flex-col overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 p-1 shadow-xl animate-in fade-in slide-in-from-top-1">
{items.map((item) => (
<button
key={item.name}
@@ -71,17 +69,19 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
e.stopPropagation();
}}
className={cn(
"flex items-center justify-between rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80",
"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-background-80": activeItem.name === item.name,
"bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name,
}
)}
>
<div className="flex items-center space-x-2">
<item.icon className="size-3 flex-shrink-0" />
<div className="rounded-sm border border-custom-border-300 p-1">
<item.icon className="h-3 w-3" />
</div>
<span>{item.name}</span>
</div>
{activeItem.name === item.name && <Check className="size-3 text-custom-text-300 flex-shrink-0" />}
{activeItem.name === item.name && <Check className="h-4 w-4" />}
</button>
))}
</section>
+4 -9
View File
@@ -1,8 +1,6 @@
import React from "react";
// ui
import { Tooltip } from "../tooltip";
// helpers
import { cn } from "../../helpers";
// types
import { TAvatarSize, getSizeInfo, isAValidNumber } from "./avatar";
@@ -57,7 +55,7 @@ export const AvatarGroup: React.FC<Props> = (props) => {
const sizeInfo = getSizeInfo(size);
return (
<div className={cn("flex", sizeInfo.spacing)}>
<div className={`flex ${sizeInfo.spacing}`}>
{avatarsWithUpdatedProps.map((avatar, index) => (
<div key={index} className="rounded-full ring-1 ring-custom-background-100">
{avatar}
@@ -66,12 +64,9 @@ export const AvatarGroup: React.FC<Props> = (props) => {
{maxAvatarsToRender < totalAvatars && (
<Tooltip tooltipContent={`${totalAvatars} total`} disabled={!showTooltip}>
<div
className={cn(
"grid place-items-center rounded-full bg-custom-primary-10 text-[9px] text-custom-primary-100 ring-1 ring-custom-background-100",
{
[sizeInfo.avatarSize]: !isAValidNumber(size),
}
)}
className={`${
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
} grid place-items-center rounded-full bg-custom-primary-10 text-[9px] text-custom-primary-100 ring-1 ring-custom-background-100`}
style={
isAValidNumber(size)
? {
+7 -12
View File
@@ -1,8 +1,6 @@
import React from "react";
// ui
import { Tooltip } from "../tooltip";
// helpers
import { cn } from "../../helpers";
export type TAvatarSize = "sm" | "md" | "base" | "lg" | number;
@@ -132,9 +130,9 @@ export const Avatar: React.FC<Props> = (props) => {
return (
<Tooltip tooltipContent={fallbackText ?? name ?? "?"} disabled={!showTooltip}>
<div
className={cn("grid place-items-center overflow-hidden", getBorderRadius(shape), {
[sizeInfo.avatarSize]: !isAValidNumber(size),
})}
className={`${
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
} grid place-items-center overflow-hidden ${getBorderRadius(shape)}`}
style={
isAValidNumber(size)
? {
@@ -146,15 +144,12 @@ export const Avatar: React.FC<Props> = (props) => {
tabIndex={-1}
>
{src ? (
<img src={src} className={cn("h-full w-full", getBorderRadius(shape), className)} alt={name} />
<img src={src} className={`h-full w-full ${getBorderRadius(shape)} ${className}`} alt={name} />
) : (
<div
className={cn(
sizeInfo.fontSize,
"grid h-full w-full place-items-center",
getBorderRadius(shape),
className
)}
className={`${sizeInfo.fontSize} grid h-full w-full place-items-center ${getBorderRadius(
shape
)} ${className}`}
style={{
backgroundColor: fallbackBackgroundColor ?? "rgba(var(--color-primary-500))",
color: fallbackTextColor ?? "#ffffff",
+2 -3
View File
@@ -1,7 +1,6 @@
import * as React from "react";
// helpers
import { getIconStyling, getBadgeStyling, TBadgeVariant, TBadgeSizes } from "./helper";
import { cn } from "../../helpers";
export interface BadgeProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: TBadgeVariant;
@@ -32,7 +31,7 @@ const Badge = React.forwardRef<HTMLButtonElement, BadgeProps>((props, ref) => {
const buttonIconStyle = getIconStyling(size);
return (
<button ref={ref} type={type} className={cn(buttonStyle, className)} disabled={disabled || loading} {...rest}>
<button ref={ref} type={type} className={`${buttonStyle} ${className}`} disabled={disabled || loading} {...rest}>
{prependIcon && <div className={buttonIconStyle}>{React.cloneElement(prependIcon, { strokeWidth: 2 })}</div>}
{children}
{appendIcon && <div className={buttonIconStyle}>{React.cloneElement(appendIcon, { strokeWidth: 2 })}</div>}
+13 -24
View File
@@ -1,7 +1,6 @@
import * as React from "react";
import { Switch } from "@headlessui/react";
// helpers
import { cn } from "../../helpers";
interface IToggleSwitchProps {
value: boolean;
@@ -20,32 +19,22 @@ const ToggleSwitch: React.FC<IToggleSwitchProps> = (props) => {
checked={value}
disabled={disabled}
onChange={onChange}
className={cn(
"relative inline-flex flex-shrink-0 h-6 w-10 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none bg-gray-700",
{
"h-4 w-6": size === "sm",
"h-5 w-8": size === "md",
"bg-custom-primary-100": value,
"cursor-not-allowed": disabled,
},
className
)}
className={`relative inline-flex flex-shrink-0 ${
size === "sm" ? "h-4 w-6" : size === "md" ? "h-5 w-8" : "h-6 w-10"
} flex-shrink-0 cursor-pointer rounded-full border border-custom-border-200 transition-colors duration-200 ease-in-out focus:outline-none ${
value ? "bg-custom-primary-100" : "bg-gray-700"
} ${className || ""} ${disabled ? "cursor-not-allowed" : ""}`}
>
<span className="sr-only">{label}</span>
<span
aria-hidden="true"
className={cn(
"inline-block self-center h-4 w-4 transform rounded-full shadow ring-0 transition duration-200 ease-in-out",
{
"translate-x-5 bg-white": value,
"h-2 w-2": size === "sm",
"h-3 w-3": size === "md",
"translate-x-3": value && size === "sm",
"translate-x-4": value && size === "md",
"translate-x-0.5 bg-custom-background-90": !value,
"cursor-not-allowed": disabled,
}
)}
className={`inline-block self-center ${
size === "sm" ? "h-2 w-2" : size === "md" ? "h-3 w-3" : "h-4 w-4"
} transform rounded-full shadow ring-0 transition duration-200 ease-in-out ${
value
? (size === "sm" ? "translate-x-3" : size === "md" ? "translate-x-4" : "translate-x-5") + " bg-white"
: "translate-x-0.5 bg-custom-background-90"
} ${disabled ? "cursor-not-allowed" : ""}`}
/>
</Switch>
);
+4 -14
View File
@@ -6,11 +6,10 @@ export type TControlLink = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
children: React.ReactNode;
target?: string;
disabled?: boolean;
className?: string;
};
export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((props, ref) => {
const { href, onClick, children, target = "_self", disabled = false, className, ...rest } = props;
export const ControlLink: React.FC<TControlLink> = (props) => {
const { href, onClick, children, target = "_self", disabled = false, ...rest } = props;
const LEFT_CLICK_EVENT_CODE = 0;
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -21,20 +20,11 @@ export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((pr
}
};
// if disabled but still has a ref or a className then it has to be rendered without a href
if (disabled && (ref || className))
return (
<a ref={ref} className={className}>
{children}
</a>
);
// else if just disabled return without the parent wrapper
if (disabled) return <>{children}</>;
return (
<a href={href} target={target} onClick={handleOnClick} {...rest} ref={ref} className={className}>
<a href={href} target={target} onClick={handleOnClick} {...rest}>
{children}
</a>
);
});
};
@@ -1,2 +0,0 @@
export * from "./item";
export * from "./root";
@@ -1,54 +0,0 @@
import React from "react";
// helpers
import { cn } from "../../../helpers";
// types
import { TContextMenuItem } from "./root";
type ContextMenuItemProps = {
handleActiveItem: () => void;
handleClose: () => void;
isActive: boolean;
item: TContextMenuItem;
};
export const ContextMenuItem: React.FC<ContextMenuItemProps> = (props) => {
const { handleActiveItem, handleClose, isActive, item } = props;
if (item.shouldRender === false) return null;
return (
<button
type="button"
className={cn(
"w-full flex items-center gap-2 px-1 py-1.5 text-left text-custom-text-200 rounded text-xs select-none",
{
"bg-custom-background-90": isActive,
"text-custom-text-400": item.disabled,
},
item.className
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action();
if (item.closeOnClick !== false) handleClose();
}}
onMouseEnter={handleActiveItem}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</button>
);
};
@@ -1,157 +0,0 @@
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
// components
import { ContextMenuItem } from "./item";
// helpers
import { cn } from "../../../helpers";
// hooks
import useOutsideClickDetector from "../../hooks/use-outside-click-detector";
export type TContextMenuItem = {
key: string;
title: string;
description?: string;
icon?: React.FC<any>;
action: () => void;
shouldRender?: boolean;
closeOnClick?: boolean;
disabled?: boolean;
className?: string;
iconClassName?: string;
};
type ContextMenuProps = {
parentRef: React.RefObject<HTMLElement>;
items: TContextMenuItem[];
};
const ContextMenuWithoutPortal: React.FC<ContextMenuProps> = (props) => {
const { parentRef, items } = props;
// states
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({
x: 0,
y: 0,
});
const [activeItemIndex, setActiveItemIndex] = useState<number>(0);
// refs
const contextMenuRef = useRef<HTMLDivElement>(null);
// derived values
const renderedItems = items.filter((item) => item.shouldRender !== false);
const handleClose = () => {
setIsOpen(false);
setActiveItemIndex(0);
};
// calculate position of context menu
useEffect(() => {
const parentElement = parentRef.current;
const contextMenu = contextMenuRef.current;
if (!parentElement || !contextMenu) return;
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const contextMenuWidth = contextMenu.clientWidth;
const contextMenuHeight = contextMenu.clientHeight;
const clickX = e?.pageX || 0;
const clickY = e?.pageY || 0;
// check if there's enough space at the bottom, otherwise show at the top
let top = clickY;
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;
setPosition({ x: left, y: top });
setIsOpen(true);
};
const hideContextMenu = (e: KeyboardEvent) => {
if (isOpen && e.key === "Escape") handleClose();
};
parentElement.addEventListener("contextmenu", handleContextMenu);
window.addEventListener("keydown", hideContextMenu);
return () => {
parentElement.removeEventListener("contextmenu", handleContextMenu);
window.removeEventListener("keydown", hideContextMenu);
};
}, [contextMenuRef, isOpen, parentRef, setIsOpen, setPosition]);
// handle keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveItemIndex((prev) => (prev + 1) % renderedItems.length);
}
if (e.key === "ArrowUp") {
e.preventDefault();
setActiveItemIndex((prev) => (prev - 1 + renderedItems.length) % renderedItems.length);
}
if (e.key === "Enter") {
e.preventDefault();
const item = renderedItems[activeItemIndex];
if (!item.disabled) {
renderedItems[activeItemIndex].action();
if (item.closeOnClick !== false) handleClose();
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [activeItemIndex, isOpen, renderedItems, setIsOpen]);
// close on clicking outside
useOutsideClickDetector(contextMenuRef, handleClose);
return (
<div
className={cn(
"fixed h-screen w-screen top-0 left-0 cursor-default z-20 opacity-0 pointer-events-none transition-opacity",
{
"opacity-100 pointer-events-auto": isOpen,
}
)}
>
<div
ref={contextMenuRef}
className="fixed border-[0.5px] border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-rg rounded-md px-2 py-2.5 max-h-72 min-w-[12rem] overflow-y-scroll vertical-scrollbar scrollbar-sm"
style={{
top: position.y,
left: position.x,
}}
>
{renderedItems.map((item, index) => (
<ContextMenuItem
key={item.key}
handleActiveItem={() => setActiveItemIndex(index)}
handleClose={handleClose}
isActive={index === activeItemIndex}
item={item}
/>
))}
</div>
</div>
);
};
export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
let contextMenu = <ContextMenuWithoutPortal {...props} />;
const portal = document.querySelector("#context-menu-portal");
if (portal) contextMenu = ReactDOM.createPortal(contextMenu, portal);
return contextMenu;
};
-1
View File
@@ -1,4 +1,3 @@
export * from "./context-menu";
export * from "./custom-menu";
export * from "./custom-select";
export * from "./custom-search-select";
+18 -19
View File
@@ -1,6 +1,4 @@
import * as React from "react";
// helpers
import { cn } from "../../helpers";
export interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
intermediate?: boolean;
@@ -11,30 +9,32 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>((props, ref)
const { id, name, checked, intermediate = false, disabled, className = "", ...rest } = props;
return (
<div className={cn("relative w-full flex gap-2", className)}>
<div className={`relative w-full flex gap-2 ${className}`}>
<input
id={id}
ref={ref}
type="checkbox"
name={name}
checked={checked}
className={cn(
"appearance-none shrink-0 w-4 h-4 border rounded-[3px] focus:outline-1 focus:outline-offset-4 focus:outline-custom-primary-50",
{
"border-custom-border-200 bg-custom-background-80 cursor-not-allowed": disabled,
"cursor-pointer border-custom-border-300 hover:border-custom-border-400 bg-white": !disabled,
"border-custom-primary-40 bg-custom-primary-100 hover:bg-custom-primary-200":
!disabled && (checked || intermediate),
className={`
appearance-none shrink-0 w-4 h-4 border rounded-[3px] focus:outline-1 focus:outline-offset-4 focus:outline-custom-primary-50
${
disabled
? "border-custom-border-200 bg-custom-background-80 cursor-not-allowed"
: `cursor-pointer ${
checked || intermediate
? "border-custom-primary-40 bg-custom-primary-100 hover:bg-custom-primary-200"
: "border-custom-border-300 hover:border-custom-border-400 bg-white"
}`
}
)}
`}
disabled={disabled}
{...rest}
/>
<svg
className={cn("absolute w-4 h-4 p-0.5 pointer-events-none outline-none hidden stroke-white", {
block: checked,
"stroke-custom-text-400 opacity-40": disabled,
})}
className={`absolute w-4 h-4 p-0.5 pointer-events-none outline-none ${
disabled ? "stroke-custom-text-400 opacity-40" : "stroke-white"
} ${checked ? "block" : "hidden"}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
@@ -46,10 +46,9 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>((props, ref)
<polyline points="20 6 9 17 4 12" />
</svg>
<svg
className={cn("absolute w-4 h-4 p-0.5 pointer-events-none outline-none stroke-white hidden", {
"stroke-custom-text-400 opacity-40": disabled,
block: intermediate && !checked,
})}
className={`absolute w-4 h-4 p-0.5 pointer-events-none outline-none ${
disabled ? "stroke-custom-text-400 opacity-40" : "stroke-white"
} ${intermediate && !checked ? "block" : "hidden"}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 8 8"
fill="none"
@@ -5,8 +5,6 @@ import { ColorResult, SketchPicker } from "react-color";
import { Input } from "./input";
import { usePopper } from "react-popper";
import { Button } from "../button";
// helpers
import { cn } from "../../helpers";
export interface InputColorPickerProps {
hasError: boolean;
@@ -47,7 +45,7 @@ export const InputColorPicker: React.FC<InputColorPickerProps> = (props) => {
onChange={handleInputChange}
hasError={hasError}
placeholder={placeholder}
className={cn("border-[0.5px] border-custom-border-200", className)}
className={`border-[0.5px] border-custom-border-200 ${className}`}
style={style}
/>
+9 -10
View File
@@ -19,16 +19,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
type={type}
name={name}
className={cn(
"block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none",
{
"rounded-md border-[0.5px] border-custom-border-200": mode === "primary",
"rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary":
mode === "transparent",
"rounded border-none bg-transparent ring-0": mode === "true-transparent",
"border-red-500": hasError,
"px-3 py-2": inputSize === "sm",
"p-3": inputSize === "md",
},
`block rounded-md bg-transparent text-sm placeholder-custom-text-400 focus:outline-none ${
mode === "primary"
? "rounded-md border-[0.5px] border-custom-border-200"
: mode === "transparent"
? "rounded border-none bg-transparent ring-0 transition-all focus:ring-1 focus:ring-custom-primary"
: mode === "true-transparent"
? "rounded border-none bg-transparent ring-0"
: ""
} ${hasError ? "border-red-500" : ""} ${inputSize === "sm" ? "px-3 py-2" : inputSize === "md" ? "p-3" : ""}`,
className
)}
{...rest}
+1 -1
View File
@@ -15,7 +15,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>((props, re
// refs
const textAreaRef = useRef<any>(ref);
// auto re-size
useAutoResizeTextArea(textAreaRef, value);
useAutoResizeTextArea(textAreaRef);
return (
<textarea
@@ -1,16 +1,24 @@
import { useLayoutEffect } from "react";
import { useEffect } from "react";
export const useAutoResizeTextArea = (
textAreaRef: React.RefObject<HTMLTextAreaElement>,
value: string | number | readonly string[]
) => {
useLayoutEffect(() => {
export const useAutoResizeTextArea = (textAreaRef: React.RefObject<HTMLTextAreaElement>) => {
useEffect(() => {
const textArea = textAreaRef.current;
if (!textArea) return;
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
textArea.style.height = "0px";
const scrollHeight = textArea.scrollHeight;
textArea.style.height = scrollHeight + "px";
}, [textAreaRef, value]);
const resizeTextArea = () => {
textArea.style.height = "auto";
const computedHeight = textArea.scrollHeight + "px";
textArea.style.height = computedHeight;
};
const handleInput = () => resizeTextArea();
// resize on mount
resizeTextArea();
textArea.addEventListener("input", handleInput);
return () => {
textArea.removeEventListener("input", handleInput);
};
}, [textAreaRef]);
};
@@ -0,0 +1,26 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const AdminProfileIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-2`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path d="M12 22C12 22 20 18 20 12V5L12 2L4 5V12C4 18 12 22 12 22Z" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M8 19V18C8 16.9391 8.42143 15.9217 9.17157 15.1716C9.92172 14.4214 10.9391 14 12 14C13.0609 14 14.0783 14.4214 14.8284 15.1716C15.5786 15.9217 16 16.9391 16 18V19"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 14C13.6569 14 15 12.6569 15 11C15 9.34315 13.6569 8 12 8C10.3431 8 9 9.34315 9 11C9 12.6569 10.3431 14 12 14Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
+25
View File
@@ -0,0 +1,25 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const CopyIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-2`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M20 8H10C8.89543 8 8 8.89543 8 10V20C8 21.1046 8.89543 22 10 22H20C21.1046 22 22 21.1046 22 20V10C22 8.89543 21.1046 8 20 8Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4 16C2.9 16 2 15.1 2 14V4C2 2.9 2.9 2 4 2H14C15.1 2 16 2.9 16 4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
@@ -0,0 +1,22 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const ExternalLinkIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-[1.5]`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M18 13V19C18 19.5304 17.7893 20.0391 17.4142 20.4142C17.0391 20.7893 16.5304 21 16 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V8C3 7.46957 3.21071 6.96086 3.58579 6.58579C3.96086 6.21071 4.46957 6 5 6H11"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M15 3H21V9" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10 14L21 3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
+22 -17
View File
@@ -1,22 +1,27 @@
export * from "./cycle";
export * from "./module";
export * from "./state";
export * from "./archive-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./calendar-after-icon";
export * from "./calendar-before-icon";
export * from "./center-panel-icon";
export * from "./create-icon";
export * from "./user-group-icon";
export * from "./dice-icon";
export * from "./discord-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./layer-stack";
export * from "./layers-icon";
export * from "./photo-filter-icon";
export * from "./priority-icon";
export * from "./related-icon";
export * from "./archive-icon";
export * from "./admin-profile-icon";
export * from "./create-icon";
export * from "./subscribe-icon";
export * from "./external-link-icon";
export * from "./copy-icon";
export * from "./layer-stack";
export * from "./side-panel-icon";
export * from "./center-panel-icon";
export * from "./full-screen-panel-icon";
export * from "./priority-icon";
export * from "./state";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./related-icon";
export * from "./module";
export * from "./cycle";
export * from "./github-icon";
export * from "./discord-icon";
export * from "./transfer-icon";
export * from "./user-group-icon";
export * from "./running-icon";
export * from "./calendar-before-icon";
export * from "./calendar-after-icon";
+9
View File
@@ -0,0 +1,9 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const RunningIcon: React.FC<ISvgIcons> = ({ className = "fill-current", ...rest }) => (
<svg viewBox="0 0 24 24" className={`${className} `} fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<path d="M4.05 12L3.2625 11.2125L10.9125 3.5625H8.25V5.0625H7.125V2.4375H11.3063C11.4813 2.4375 11.65 2.46875 11.8125 2.53125C11.975 2.59375 12.1188 2.6875 12.2438 2.8125L14.4938 5.04375C14.8563 5.40625 15.275 5.68125 15.75 5.86875C16.225 6.05625 16.725 6.1625 17.25 6.1875V7.3125C16.6 7.2875 15.975 7.16563 15.375 6.94688C14.775 6.72813 14.25 6.3875 13.8 5.925L12.9375 5.0625L10.8 7.2L12.4125 8.8125L7.8375 11.4563L7.275 10.4813L10.575 8.56875L9.0375 7.03125L4.05 12ZM2.25 6.75V5.625H6V6.75H2.25ZM0.75 4.3125V3.1875H4.5V4.3125H0.75ZM14.8125 2.8125C14.45 2.8125 14.1406 2.68438 13.8844 2.42813C13.6281 2.17188 13.5 1.8625 13.5 1.5C13.5 1.1375 13.6281 0.828125 13.8844 0.571875C14.1406 0.315625 14.45 0.1875 14.8125 0.1875C15.175 0.1875 15.4844 0.315625 15.7406 0.571875C15.9969 0.828125 16.125 1.1375 16.125 1.5C16.125 1.8625 15.9969 2.17188 15.7406 2.42813C15.4844 2.68438 15.175 2.8125 14.8125 2.8125ZM2.25 1.875V0.75H6V1.875H2.25Z" />
</svg>
);
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const SubscribeIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg
viewBox="0 0 24 24"
className={`${className} stroke-2`}
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M21 12V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H12"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15 5.1C15 4.54305 15.2107 4.0089 15.5858 3.61508C15.9609 3.22125 16.4696 3 17 3C17.5304 3 18.0391 3.22125 18.4142 3.61508C18.7893 4.0089 19 4.54305 19 5.1C19 7.55 20 8.25 20 8.25H14C14 8.25 15 7.55 15 5.1Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.25 11C16.3238 11 16.4324 11 16.5643 11C16.6963 11 16.8467 11 17 11C17.1533 11 17.3037 11 17.4357 11C17.5676 11 17.6762 11 17.75 11"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
+1 -3
View File
@@ -1,6 +1,4 @@
import React from "react";
// helpers
import { cn } from "../helpers";
type Props = {
children: React.ReactNode;
@@ -8,7 +6,7 @@ type Props = {
};
const Loader = ({ children, className = "" }: Props) => (
<div className={cn("animate-pulse", className)} role="status">
<div className={`${className} animate-pulse`} role="status">
{children}
</div>
);
@@ -1,6 +1,4 @@
import * as React from "react";
// helpers
import { cn } from "../../helpers";
export interface ISpinner extends React.SVGAttributes<SVGElement> {
height?: string;
@@ -14,7 +12,7 @@ export const Spinner: React.FC<ISpinner> = ({ height = "32px", width = "32px", c
aria-hidden="true"
height={height}
width={width}
className={cn("animate-spin fill-blue-600 text-custom-text-200", className)}
className={`animate-spin fill-blue-600 text-custom-text-200 ${className}`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
+5 -9
View File
@@ -1,7 +1,7 @@
import React from "react";
// next-themes
import { Tooltip2 } from "@blueprintjs/popover2";
// helpers
import { cn } from "../../helpers";
export type TPosition =
| "top"
@@ -49,13 +49,9 @@ export const Tooltip: React.FC<ITooltipProps> = ({
hoverCloseDelay={closeDelay}
content={
<div
className={cn(
"relative block z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md",
{
hidden: isMobile,
},
className
)}
className={`relative ${
isMobile ? "hidden" : "block"
} z-50 max-w-xs gap-1 overflow-hidden break-words rounded-md bg-custom-background-100 p-2 text-xs text-custom-text-200 shadow-md ${className}`}
>
{tooltipHeading && <h5 className="font-medium text-custom-text-100">{tooltipHeading}</h5>}
{tooltipContent}
+7 -13
View File
@@ -4,9 +4,6 @@ import {
Heading1,
Heading2,
Heading3,
Heading4,
Heading5,
Heading6,
Image,
Italic,
List,
@@ -32,17 +29,14 @@ export type ToolbarMenuItem = {
};
export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [
{ key: "h1", name: "Heading 1", icon: Heading1, editors: ["document"] },
{ key: "h2", name: "Heading 2", icon: Heading2, editors: ["document"] },
{ key: "h3", name: "Heading 3", icon: Heading3, editors: ["document"] },
{ key: "h4", name: "Heading 4", icon: Heading4, editors: ["document"] },
{ key: "h5", name: "Heading 5", icon: Heading5, editors: ["document"] },
{ key: "h6", name: "Heading 6", icon: Heading6, editors: ["document"] },
{ key: "H1", name: "Heading 1", icon: Heading1, editors: ["document"] },
{ key: "H2", name: "Heading 2", icon: Heading2, editors: ["document"] },
{ key: "H3", name: "Heading 3", icon: Heading3, editors: ["document"] },
{ key: "bold", name: "Bold", icon: Bold, shortcut: ["Cmd", "B"], editors: ["lite", "document"] },
{ key: "italic", name: "Italic", icon: Italic, shortcut: ["Cmd", "I"], editors: ["lite", "document"] },
{ key: "underline", name: "Underline", icon: Underline, shortcut: ["Cmd", "U"], editors: ["lite", "document"] },
{
key: "strikethrough",
key: "strike",
name: "Strikethrough",
icon: Strikethrough,
shortcut: ["Cmd", "Shift", "S"],
@@ -52,21 +46,21 @@ export const BASIC_MARK_ITEMS: ToolbarMenuItem[] = [
export const LIST_ITEMS: ToolbarMenuItem[] = [
{
key: "bulleted-list",
key: "bullet-list",
name: "Bulleted list",
icon: List,
shortcut: ["Cmd", "Shift", "7"],
editors: ["lite", "document"],
},
{
key: "numbered-list",
key: "ordered-list",
name: "Numbered list",
icon: ListOrdered,
shortcut: ["Cmd", "Shift", "8"],
editors: ["lite", "document"],
},
{
key: "to-do-list",
key: "To-do List",
name: "To-do list",
icon: ListTodo,
shortcut: ["Cmd", "Shift", "9"],
-1
View File
@@ -6,7 +6,6 @@ class MyDocument extends Document {
<Html>
<Head />
<body className="w-100 bg-custom-background-100 antialiased">
<div id="context-menu-portal" />
<Main />
<NextScript />
</body>
@@ -111,7 +111,7 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-2xl">
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
{generatedToken ? (
<GeneratedTokenDetails handleClose={handleClose} tokenDetails={generatedToken} />
) : (
+5 -5
View File
@@ -146,7 +146,7 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
onChange={onChange}
hasError={Boolean(errors.description)}
placeholder="Token description"
className="min-h-24 w-full text-sm"
className="h-24 w-full text-sm"
/>
)}
/>
@@ -170,8 +170,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
{value === "custom"
? "Custom date"
: selectedOption
? selectedOption.label
: "Set expiration date"}
? selectedOption.label
: "Set expiration date"}
</div>
}
value={value}
@@ -207,8 +207,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
? `Expires ${renderFormattedDate(customDate)}`
: null
: watch("expired_at")
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}`
: null}
? `Expires ${getExpiryDate(watch("expired_at") ?? "")}`
: null}
</span>
)}
</div>
@@ -28,8 +28,8 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
};
return (
<div className="w-full">
<div className="w-full space-y-3 text-wrap">
<div>
<div className="space-y-3">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">Key created</h3>
<p className="text-sm text-custom-text-400">
Copy and save this secret key in Plane Pages. You can{"'"}t see this key after you hit Close. A CSV file
@@ -39,11 +39,11 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
<button
type="button"
onClick={() => copyApiToken(tokenDetails.token ?? "")}
className="mt-4 flex truncate w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none"
className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none"
>
<span className="truncate pr-2">{tokenDetails.token}</span>
{tokenDetails.token}
<Tooltip tooltipContent="Copy secret key" isMobile={isMobile}>
<Copy className="h-4 w-4 text-custom-text-400 flex-shrink-0" />
<Copy className="h-4 w-4 text-custom-text-400" />
</Tooltip>
</button>
<div className="mt-6 flex items-center justify-between">
@@ -178,9 +178,7 @@ export const CommandModal: React.FC = observer(() => {
return 0;
}}
onKeyDown={(e) => {
// when search term is not empty, esc should clear the search term
if (e.key === "Escape" && searchTerm) setSearchTerm("");
// when search is empty and page is undefined
// when user tries to close the modal with esc
if (e.key === "Escape" && !page && !searchTerm) closePalette();
-2
View File
@@ -1,2 +0,0 @@
export * from "./list-item";
export * from "./list-root";
-56
View File
@@ -1,56 +0,0 @@
import React, { FC } from "react";
import Link from "next/link";
// ui
import { Tooltip } from "@plane/ui";
interface IListItemProps {
title: string;
itemLink: string;
onItemClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
prependTitleElement?: JSX.Element;
appendTitleElement?: JSX.Element;
actionableItems?: JSX.Element;
isMobile?: boolean;
parentRef: React.RefObject<HTMLDivElement>;
}
export const ListItem: FC<IListItemProps> = (props) => {
const {
title,
prependTitleElement,
appendTitleElement,
actionableItems,
itemLink,
onItemClick,
isMobile = false,
parentRef,
} = props;
return (
<div ref={parentRef} className="relative">
<Link href={itemLink} onClick={onItemClick}>
<div className="group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row">
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
{prependTitleElement && <span className="flex items-center flex-shrink-0">{prependTitleElement}</span>}
<Tooltip tooltipContent={title} position="top" isMobile={isMobile}>
<span className="truncate text-sm">{title}</span>
</Tooltip>
</div>
{appendTitleElement && <span className="flex items-center flex-shrink-0">{appendTitleElement}</span>}
</div>
</div>
<span className="h-6 w-96 flex-shrink-0" />
</div>
</Link>
{actionableItems && (
<div className="absolute right-5 bottom-4 flex items-center gap-1.5">
<div className="relative flex items-center gap-4 sm:w-auto sm:flex-shrink-0 sm:justify-end">
{actionableItems}
</div>
</div>
)}
</div>
);
};
-10
View File
@@ -1,10 +0,0 @@
import React, { FC } from "react";
interface IListContainer {
children: React.ReactNode;
}
export const ListLayout: FC<IListContainer> = (props) => {
const { children } = props;
return <div className="flex h-full w-full flex-col overflow-y-auto vertical-scrollbar scrollbar-lg">{children}</div>;
};
@@ -36,7 +36,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
workspaceLevelToggle = false,
} = props;
// states
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
@@ -73,7 +72,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsLoading(true);
projectService
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
search: debouncedSearchTerm,
@@ -81,10 +80,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
workspace_search: isWorkspaceLevel,
})
.then((res) => setIssues(res))
.finally(() => {
setIsSearching(false);
setIsLoading(false);
});
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
return (
@@ -198,7 +194,14 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
</h5>
)}
{isSearching || isLoading ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
@@ -206,59 +209,48 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
<Loader.Item height="40px" />
</Loader>
) : (
<>
{issues.length === 0 ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
) : (
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex items-center gap-2">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>
{issue.name}
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
);
})}
</ul>
)}
</>
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex items-center gap-2">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>
{issue.name}
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
);
})}
</ul>
)}
</Combobox.Options>
</Combobox>
+85 -89
View File
@@ -1,9 +1,9 @@
import { observer } from "mobx-react";
// icons
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
import { Pencil, Trash2, LinkIcon } from "lucide-react";
import { ILinkDetails, UserAuth } from "@plane/types";
// ui
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { calculateTimeAgo } from "@/helpers/date-time.helper";
// hooks
@@ -19,95 +19,91 @@ type Props = {
disabled?: boolean;
};
export const LinksList: React.FC<Props> = observer(
({ links, handleDeleteLink, handleEditLink, userAuth, disabled }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth, disabled }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
});
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Copied to clipboard",
message: "The URL has been successfully copied to your clipboard",
});
};
return (
<>
{links.map((link) => {
const createdByDetails = getUserDetails(link.created_by);
return (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
return (
<>
{links.map((link) => {
const createdByDetails = getUserDetails(link.created_by);
return (
<div key={link.id} className="relative flex flex-col rounded-md bg-custom-background-90 p-2.5">
<div className="flex w-full items-start justify-between gap-2">
<div className="flex items-start gap-2 truncate">
<span className="py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" />
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
<span
className="cursor-pointer truncate text-xs"
onClick={() => copyToClipboard(link.title && link.title !== "" ? link.title : link.url)}
>
{link.title && link.title !== "" ? link.title : link.url}
</span>
</Tooltip>
</div>
</Tooltip>
</div>
{!isNotAllowed && (
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink(link);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
>
<ExternalLink className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink(link.id);
}}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
)}
</div>
<div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}
<br />
{createdByDetails && (
<>
by{" "}
{createdByDetails?.is_bot
? createdByDetails?.first_name + " Bot"
: createdByDetails?.display_name}
</>
)}
</p>
</div>
{!isNotAllowed && (
<div className="z-[1] flex flex-shrink-0 items-center gap-2">
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEditLink(link);
}}
>
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</button>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
>
<ExternalLinkIcon className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
</a>
<button
type="button"
className="flex items-center justify-center p-1 hover:bg-custom-background-80"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleDeleteLink(link.id);
}}
>
<Trash2 className="h-3 w-3" />
</button>
</div>
)}
</div>
);
})}
</>
);
}
);
<div className="px-5">
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
Added {calculateTimeAgo(link.created_at)}
<br />
{createdByDetails && (
<>
by{" "}
{createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}
</>
)}
</p>
</div>
</div>
);
})}
</>
);
});
@@ -4,8 +4,6 @@ import Image from "next/image";
// headless ui
import { Tab } from "@headlessui/react";
import {
IIssueFilterOptions,
IIssueFilters,
IModule,
TAssigneesDistribution,
TCompletionChartDistribution,
@@ -39,9 +37,6 @@ type Props = {
roundedTab?: boolean;
noBackground?: boolean;
isPeekView?: boolean;
isCompleted?: boolean;
filters?: IIssueFilters | undefined;
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
};
export const SidebarProgressStats: React.FC<Props> = ({
@@ -52,9 +47,6 @@ export const SidebarProgressStats: React.FC<Props> = ({
roundedTab,
noBackground,
isPeekView = false,
isCompleted = false,
filters,
handleFiltersUpdate,
}) => {
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
@@ -153,11 +145,20 @@ export const SidebarProgressStats: React.FC<Props> = ({
}
completed={assignee.completed_issues}
total={assignee.total_issues}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("assignees", assignee.assignee_id ?? ""),
selected: filters?.filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
{...(!isPeekView && {
onClick: () => {
// TODO: set filters here
// if (filters?.assignees?.includes(assignee.assignee_id ?? ""))
// setFilters({
// assignees: filters?.assignees?.filter((a) => a !== assignee.assignee_id),
// });
// else
// setFilters({
// assignees: [...(filters?.assignees ?? []), assignee.assignee_id ?? ""],
// });
},
// selected: filters?.assignees?.includes(assignee.assignee_id ?? ""),
})}
/>
);
else
@@ -191,52 +192,35 @@ export const SidebarProgressStats: React.FC<Props> = ({
className="flex w-full flex-col gap-1.5 overflow-y-auto pt-3.5 vertical-scrollbar scrollbar-sm"
>
{distribution && distribution?.labels.length > 0 ? (
distribution.labels.map((label, index) => {
if (label.label_id) {
return (
<SingleProgressStats
key={label.label_id}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekView &&
!isCompleted && {
onClick: () => handleFiltersUpdate("labels", label.label_id ?? ""),
selected: filters?.filters?.labels?.includes(label.label_id ?? `no-label-${index}`),
})}
/>
);
} else {
return (
<SingleProgressStats
key={`no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
/>
);
}
})
distribution.labels.map((label, index) => (
<SingleProgressStats
key={label.label_id ?? `no-label-${index}`}
title={
<div className="flex items-center gap-2">
<span
className="block h-3 w-3 rounded-full"
style={{
backgroundColor: label.color ?? "transparent",
}}
/>
<span className="text-xs">{label.label_name ?? "No labels"}</span>
</div>
}
completed={label.completed_issues}
total={label.total_issues}
{...(!isPeekView && {
// TODO: set filters here
onClick: () => {
// if (filters.labels?.includes(label.label_id ?? ""))
// setFilters({
// labels: filters?.labels?.filter((l) => l !== label.label_id),
// });
// else setFilters({ labels: [...(filters?.labels ?? []), label.label_id ?? ""] });
},
// selected: filters?.labels?.includes(label.label_id ?? ""),
})}
/>
))
) : (
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
@@ -1,4 +1,3 @@
import { useRef } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -21,8 +20,6 @@ type Props = {
export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
const { cycleId } = props;
// refs
const parentRef = useRef(null);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -93,7 +90,6 @@ export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
return (
<Link
ref={parentRef}
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`}
className="py-5 px-2 flex items-center justify-between gap-2 hover:bg-custom-background-90"
>
@@ -127,7 +123,6 @@ export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
{workspaceSlug && projectId && (
<CycleQuickActions
parentRef={parentRef}
cycleId={cycleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
@@ -1,4 +1,4 @@
import { FC, MouseEvent, useRef } from "react";
import { FC, MouseEvent } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -28,8 +28,6 @@ export interface ICyclesBoardCard {
export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// refs
const parentRef = useRef(null);
// router
const router = useRouter();
// store
@@ -151,8 +149,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return (
<div className="relative">
<Link ref={parentRef} href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-3 truncate">
@@ -233,28 +231,23 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions cycleId={cycleId} projectId={projectId} workspaceSlug={workspaceSlug} />
</div>
</div>
</div>
</div>
</Link>
<div className="absolute right-4 bottom-3.5 flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions
parentRef={parentRef}
cycleId={cycleId}
projectId={projectId}
workspaceSlug={workspaceSlug}
/>
</div>
</div>
);
});
+1 -1
View File
@@ -76,7 +76,7 @@ export const CyclesViewHeader: React.FC<Props> = observer((props) => {
};
return (
<div className="h-[50px] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-6 sm:pb-0">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-custom-border-200 px-4 sm:px-5 sm:pb-0">
<Tab.List as="div" className="flex items-center overflow-x-scroll">
{CYCLE_TABS_LIST.map((tab) => (
<Tab
@@ -1,156 +0,0 @@
import React, { FC, MouseEvent } from "react";
import { observer } from "mobx-react";
import { User2 } from "lucide-react";
// types
import { ICycle, TCycleGroups } from "@plane/types";
// ui
import { Avatar, AvatarGroup, Tooltip, setPromiseToast } from "@plane/ui";
// components
import { FavoriteStar } from "@/components/core";
import { CycleQuickActions } from "@/components/cycles";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useCycle, useEventTracker, useMember, useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
workspaceSlug: string;
projectId: string;
cycleId: string;
cycleDetails: ICycle;
parentRef: React.RefObject<HTMLDivElement>;
};
export const CycleListItemAction: FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId, cycleDetails, parentRef } = props;
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const { captureEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
const { getUserDetails } = useMember();
// derived values
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const renderDate = cycleDetails.start_date || cycleDetails.end_date;
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
// handlers
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
}
);
setPromiseToast(addToFavoritePromise, {
loading: "Adding cycle to favorites...",
success: {
title: "Success!",
message: () => "Cycle added to favorites.",
},
error: {
title: "Error!",
message: () => "Couldn't add the cycle to favorites. Please try again.",
},
});
};
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const removeFromFavoritePromise = removeCycleFromFavorites(
workspaceSlug?.toString(),
projectId.toString(),
cycleId
).then(() => {
captureEvent(CYCLE_UNFAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
});
setPromiseToast(removeFromFavoritePromise, {
loading: "Removing cycle from favorites...",
success: {
title: "Success!",
message: () => "Cycle removed from favorites.",
},
error: {
title: "Error!",
message: () => "Couldn't remove the cycle from favorites. Please try again.",
},
});
};
return (
<>
<div className="text-xs text-custom-text-300 flex-shrink-0">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
{currentCycle && (
<div
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current"
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</div>
)}
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => {
const member = getUserDetails(assignee_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !cycleDetails.archived_at && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions parentRef={parentRef} cycleId={cycleId} projectId={projectId} workspaceSlug={workspaceSlug} />
</>
);
});
+201 -67
View File
@@ -1,17 +1,24 @@
import { FC, MouseEvent, useRef } from "react";
import { FC, MouseEvent } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
// icons
import { Check, Info } from "lucide-react";
import { Check, Info, User2 } from "lucide-react";
// types
import type { TCycleGroups } from "@plane/types";
// ui
import { CircularProgressIndicator } from "@plane/ui";
import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui";
// components
import { ListItem } from "@/components/core/list";
import { CycleListItemAction } from "@/components/cycles/list";
import { FavoriteStar } from "@/components/core";
import { CycleQuickActions } from "@/components/cycles";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useCycle } from "@/hooks/store";
import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TCyclesListItem = {
@@ -22,41 +29,79 @@ type TCyclesListItem = {
handleRemoveFromFavorites?: () => void;
workspaceSlug: string;
projectId: string;
isArchived?: boolean;
};
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const { cycleId, workspaceSlug, projectId } = props;
// refs
const parentRef = useRef(null);
const { cycleId, workspaceSlug, projectId, isArchived } = props;
// router
const router = useRouter();
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const { getCycleById } = useCycle();
const { captureEvent } = useEventTracker();
const {
membership: { currentProjectRole },
} = useUser();
const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const { getUserDetails } = useMember();
// derived values
const cycleDetails = getCycleById(cycleId);
const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
if (!cycleDetails) return null;
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
}
);
// computed
// TODO: change this logic once backend fix the response
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isCompleted = cycleStatus === "completed";
setPromiseToast(addToFavoritePromise, {
loading: "Adding cycle to favorites...",
success: {
title: "Success!",
message: () => "Cycle added to favorites.",
},
error: {
title: "Error!",
message: () => "Couldn't add the cycle to favorites. Please try again.",
},
});
};
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const handleRemoveFromFavorites = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
if (!workspaceSlug || !projectId) return;
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const removeFromFavoritePromise = removeCycleFromFavorites(
workspaceSlug?.toString(),
projectId.toString(),
cycleId
).then(() => {
captureEvent(CYCLE_UNFAVORITED, {
cycle_id: cycleId,
element: "List layout",
state: "SUCCESS",
});
});
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
setPromiseToast(removeFromFavoritePromise, {
loading: "Removing cycle from favorites...",
success: {
title: "Success!",
message: () => "Cycle removed from favorites.",
},
error: {
title: "Error!",
message: () => "Couldn't remove the cycle from favorites. Please try again.",
},
});
};
// handlers
const openCycleOverview = (e: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
const { query } = router;
e.preventDefault();
@@ -76,47 +121,136 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
}
};
const cycleDetails = getCycleById(cycleId);
if (!cycleDetails) return null;
// computed
// TODO: change this logic once backend fix the response
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isCompleted = cycleStatus === "completed";
const endDate = getDate(cycleDetails.end_date);
const startDate = getDate(cycleDetails.start_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const renderDate = cycleDetails.start_date || cycleDetails.end_date;
// const areYearsEqual = startDate.getFullYear() === endDate.getFullYear();
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return (
<ListItem
title={cycleDetails?.name ?? ""}
itemLink={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}
onItemClick={(e) => {
if (cycleDetails.archived_at) openCycleOverview(e);
}}
prependTitleElement={
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
{isCompleted ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
}
appendTitleElement={
<button
onClick={openCycleOverview}
className={`z-[5] flex-shrink-0 ${isMobile ? "flex" : "hidden group-hover:flex"}`}
>
<Info className="h-4 w-4 text-custom-text-400" />
</button>
}
actionableItems={
<CycleListItemAction
workspaceSlug={workspaceSlug}
projectId={projectId}
cycleId={cycleId}
cycleDetails={cycleDetails}
parentRef={parentRef}
/>
}
isMobile={isMobile}
parentRef={parentRef}
/>
<>
<Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}
onClick={(e) => {
if (isArchived) {
openCycleOverview(e);
}
}}
>
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 md:flex-row">
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{isCompleted ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</div>
<div className="relative flex items-center gap-2.5 overflow-hidden">
<CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5 flex-shrink-0" />
<Tooltip tooltipContent={cycleDetails.name} position="top" isMobile={isMobile}>
<span className="line-clamp-1 inline-block overflow-hidden truncate text-base font-medium">
{cycleDetails.name}
</span>
</Tooltip>
</div>
<button onClick={openCycleOverview} className="invisible z-[5] flex-shrink-0 group-hover:visible">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="text-xs text-custom-text-300 flex-shrink-0">
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
</div>
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 md:w-auto md:flex-shrink-0 md:justify-end">
{currentCycle && (
<div
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current"
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</div>
)}
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => {
const member = getUserDetails(assignee_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions
cycleId={cycleId}
projectId={projectId}
workspaceSlug={workspaceSlug}
isArchived={isArchived}
/>
</div>
</div>
</div>
</Link>
</>
);
});
@@ -5,15 +5,22 @@ type Props = {
cycleIds: string[];
projectId: string;
workspaceSlug: string;
isArchived?: boolean;
};
export const CyclesListMap: React.FC<Props> = (props) => {
const { cycleIds, projectId, workspaceSlug } = props;
const { cycleIds, projectId, workspaceSlug, isArchived } = props;
return (
<>
{cycleIds.map((cycleId) => (
<CyclesListItem key={cycleId} cycleId={cycleId} workspaceSlug={workspaceSlug} projectId={projectId} />
<CyclesListItem
key={cycleId}
cycleId={cycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
isArchived={isArchived}
/>
))}
</>
);
-1
View File
@@ -1,4 +1,3 @@
export * from "./cycles-list-item";
export * from "./cycles-list-map";
export * from "./root";
export * from "./cycle-list-item-action";
+9 -4
View File
@@ -3,7 +3,6 @@ import { observer } from "mobx-react-lite";
import { ChevronRight } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// components
import { ListLayout } from "@/components/core/list";
import { CyclePeekOverview, CyclesListMap } from "@/components/cycles";
// helpers
import { cn } from "@/helpers/common.helper";
@@ -22,11 +21,12 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
return (
<div className="h-full overflow-y-auto">
<div className="flex h-full w-full justify-between">
<ListLayout>
<div className="flex h-full w-full flex-col overflow-y-auto vertical-scrollbar scrollbar-lg">
<CyclesListMap
cycleIds={cycleIds}
projectId={projectId}
workspaceSlug={workspaceSlug}
isArchived={isArchived}
/>
{completedCycleIds.length !== 0 && (
<Disclosure as="div" className="py-8 pl-3 space-y-4">
@@ -43,11 +43,16 @@ export const CyclesList: FC<ICyclesList> = observer((props) => {
)}
</Disclosure.Button>
<Disclosure.Panel>
<CyclesListMap cycleIds={completedCycleIds} projectId={projectId} workspaceSlug={workspaceSlug} />
<CyclesListMap
cycleIds={completedCycleIds}
projectId={projectId}
workspaceSlug={workspaceSlug}
isArchived={isArchived}
/>
</Disclosure.Panel>
</Disclosure>
)}
</ListLayout>
</div>
<CyclePeekOverview projectId={projectId} workspaceSlug={workspaceSlug} isArchived={isArchived} />
</div>
</div>
+78 -98
View File
@@ -2,28 +2,27 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
// icons
import { ArchiveRestoreIcon, ExternalLink, LinkIcon, Pencil, Trash2 } from "lucide-react";
import { ArchiveRestoreIcon, LinkIcon, Pencil, Trash2 } from "lucide-react";
// ui
import { ArchiveIcon, ContextMenu, CustomMenu, TContextMenuItem, TOAST_TYPE, setToast } from "@plane/ui";
import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { ArchiveCycleModal, CycleCreateUpdateModal, CycleDeleteModal } from "@/components/cycles";
// constants
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { cn } from "@/helpers/common.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useCycle, useEventTracker, useUser } from "@/hooks/store";
type Props = {
parentRef: React.RefObject<HTMLElement>;
cycleId: string;
projectId: string;
workspaceSlug: string;
isArchived?: boolean;
};
export const CycleQuickActions: React.FC<Props> = observer((props) => {
const { parentRef, cycleId, projectId, workspaceSlug } = props;
const { cycleId, projectId, workspaceSlug, isArchived } = props;
// router
const router = useRouter();
// states
@@ -38,31 +37,40 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
const { getCycleById, restoreCycle } = useCycle();
// derived values
const cycleDetails = getCycleById(cycleId);
const isArchived = !!cycleDetails?.archived_at;
const isCompleted = cycleDetails?.status?.toLowerCase() === "completed";
// auth
const isEditingAllowed =
!!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER;
const cycleLink = `${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`;
const handleCopyText = () =>
copyUrlToClipboard(cycleLink).then(() => {
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Link Copied!",
message: "Cycle link copied to clipboard.",
});
});
const handleOpenInNewTab = () => window.open(`/${cycleLink}`, "_blank");
};
const handleEditCycle = () => {
const handleEditCycle = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setTrackElement("Cycles page list layout");
setUpdateModal(true);
};
const handleArchiveCycle = () => setArchiveCycleModal(true);
const handleArchiveCycle = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setArchiveCycleModal(true);
};
const handleRestoreCycle = async () =>
const handleRestoreCycle = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
await restoreCycle(workspaceSlug, projectId, cycleId)
.then(() => {
setToast({
@@ -79,61 +87,15 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
message: "Cycle could not be restored. Please try again.",
})
);
};
const handleDeleteCycle = () => {
const handleDeleteCycle = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setTrackElement("Cycles page list layout");
setDeleteModal(true);
};
const MENU_ITEMS: TContextMenuItem[] = [
{
key: "edit",
title: "Edit",
icon: Pencil,
action: handleEditCycle,
shouldRender: isEditingAllowed && !isCompleted && !isArchived,
},
{
key: "open-new-tab",
action: handleOpenInNewTab,
title: "Open in new tab",
icon: ExternalLink,
shouldRender: !isArchived,
},
{
key: "copy-link",
action: handleCopyText,
title: "Copy link",
icon: LinkIcon,
shouldRender: !isArchived,
},
{
key: "archive",
action: handleArchiveCycle,
title: "Archive",
description: isCompleted ? undefined : "Only completed cycle can\nbe archived.",
icon: ArchiveIcon,
className: "items-start",
iconClassName: "mt-1",
shouldRender: isEditingAllowed && !isArchived,
disabled: !isCompleted,
},
{
key: "restore",
action: handleRestoreCycle,
title: "Restore",
icon: ArchiveRestoreIcon,
shouldRender: isEditingAllowed && isArchived,
},
{
key: "delete",
action: handleDeleteCycle,
title: "Delete",
icon: Trash2,
shouldRender: isEditingAllowed && !isCompleted && !isArchived,
},
];
return (
<>
{cycleDetails && (
@@ -161,42 +123,60 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
/>
</div>
)}
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
<CustomMenu ellipsis placement="bottom-end" closeOnSelect>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
<CustomMenu ellipsis placement="bottom-end">
{!isCompleted && isEditingAllowed && !isArchived && (
<CustomMenu.MenuItem onClick={handleEditCycle}>
<span className="flex items-center justify-start gap-2">
<Pencil className="h-3 w-3" />
<span>Edit cycle</span>
</span>
</CustomMenu.MenuItem>
)}
{isEditingAllowed && !isArchived && (
<CustomMenu.MenuItem onClick={handleArchiveCycle} disabled={!isCompleted}>
{isCompleted ? (
<div className="flex items-center gap-2">
<ArchiveIcon className="h-3 w-3" />
Archive cycle
</div>
</CustomMenu.MenuItem>
);
})}
) : (
<div className="flex items-start gap-2">
<ArchiveIcon className="h-3 w-3" />
<div className="-mt-1">
<p>Archive cycle</p>
<p className="text-xs text-custom-text-400">
Only completed cycle <br /> can be archived.
</p>
</div>
</div>
)}
</CustomMenu.MenuItem>
)}
{isEditingAllowed && isArchived && (
<CustomMenu.MenuItem onClick={handleRestoreCycle}>
<span className="flex items-center justify-start gap-2">
<ArchiveRestoreIcon className="h-3 w-3" />
<span>Restore cycle</span>
</span>
</CustomMenu.MenuItem>
)}
{!isArchived && (
<CustomMenu.MenuItem onClick={handleCopyText}>
<span className="flex items-center justify-start gap-2">
<LinkIcon className="h-3 w-3" />
<span>Copy cycle link</span>
</span>
</CustomMenu.MenuItem>
)}
<hr className="my-2 border-custom-border-200" />
{!isCompleted && isEditingAllowed && (
<CustomMenu.MenuItem onClick={handleDeleteCycle}>
<span className="flex items-center justify-start gap-2">
<Trash2 className="h-3 w-3" />
<span>Delete cycle</span>
</span>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</>
);
+23 -41
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import isEmpty from "lodash/isEmpty";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
@@ -16,7 +16,7 @@ import {
} from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
// types
import { ICycle, IIssueFilterOptions } from "@plane/types";
import { ICycle } from "@plane/types";
// ui
import { Avatar, ArchiveIcon, CustomMenu, Loader, LayersIcon, TOAST_TYPE, setToast, TextArea } from "@plane/ui";
// components
@@ -27,13 +27,12 @@ import { DateRangeDropdown } from "@/components/dropdowns";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_UPDATED } from "@/constants/event-tracker";
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useCycle, useUser, useMember, useIssues } from "@/hooks/store";
import { useEventTracker, useCycle, useUser, useMember } from "@/hooks/store";
// services
import { CycleService } from "@/services/cycle.service";
@@ -192,36 +191,25 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}
};
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
// TODO: refactor this
// const handleFiltersUpdate = useCallback(
// (key: keyof IIssueFilterOptions, value: string | string[]) => {
// if (!workspaceSlug || !projectId) return;
// const newValues = issueFilters?.filters?.[key] ?? [];
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
// if (Array.isArray(value)) {
// value.forEach((val) => {
// if (!newValues.includes(val)) newValues.push(val);
// });
// } else {
// if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
// else newValues.push(value);
// }
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(
workspaceSlug.toString(),
projectId.toString(),
EIssueFilterType.FILTERS,
{ [key]: newValues },
cycleId
);
},
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
);
// updateFilters(workspaceSlug.toString(), projectId.toString(), EFilterType.FILTERS, { [key]: newValues }, cycleId);
// },
// [workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
// );
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase();
const isCompleted = cycleStatus === "completed";
@@ -263,8 +251,8 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
? "0 Issue"
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
: cycleDetails.total_issues === 0
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
@@ -416,7 +404,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
to: "End date",
}}
required={cycleDetails.status !== "draft"}
disabled={!isEditingAllowed || isArchived}
disabled={isArchived}
/>
)}
/>
@@ -563,9 +551,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}}
totalIssues={cycleDetails.progress_snapshot.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
@@ -585,9 +570,6 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
isCompleted={isCompleted}
filters={issueFilters}
handleFiltersUpdate={handleFiltersUpdate}
/>
</div>
)}
+2 -3
View File
@@ -1,5 +1,4 @@
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
@@ -24,7 +23,7 @@ export type EmptyStateProps = {
secondaryButtonOnClick?: () => void;
};
export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
export const EmptyState: React.FC<EmptyStateProps> = (props) => {
const {
type,
size = "lg",
@@ -174,4 +173,4 @@ export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
)}
</>
);
});
};
@@ -256,7 +256,7 @@ export const CreateUpdateEstimateModal: React.FC<Props> = observer((props) => {
value={value}
placeholder="Description"
onChange={onChange}
className="mt-3 min-h-32 resize-none text-sm"
className="mt-3 h-32 resize-none text-sm"
hasError={Boolean(errors?.description)}
/>
)}
+3 -4
View File
@@ -36,7 +36,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
} = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { getIsIssuePeeked } = useIssueDetail();
const { peekIssue } = useIssueDetail();
const isBlockVisibleOnChart = block.start_date && block.target_date;
@@ -81,9 +81,8 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
<div
className={cn("relative h-full", {
"bg-custom-background-80": isBlockActive(block.id),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
block.data.id
),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
@@ -25,7 +25,7 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
const { block, enableReorder, provided, snapshot } = props;
// store hooks
const { updateActiveBlockId, isBlockActive } = useGanttChart();
const { getIsIssuePeeked } = useIssueDetail();
const { peekIssue } = useIssueDetail();
const duration = findTotalDaysInRange(block.start_date, block.target_date);
@@ -33,9 +33,8 @@ export const IssuesSidebarBlock: React.FC<Props> = observer((props) => {
<div
className={cn({
"rounded bg-custom-background-80": snapshot.isDragging,
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(
block.data.id
),
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
peekIssue?.issueId === block.data.id,
})}
onMouseEnter={() => updateActiveBlockId(block.id)}
onMouseLeave={() => updateActiveBlockId(null)}
+46 -43
View File
@@ -1,16 +1,15 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// icons
import { Plus } from "lucide-react";
// hooks
// ui
import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui";
// helpers
// components
import { BreadcrumbLink } from "@/components/common";
import { ProjectLogo } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
// hooks
import { useApplication, useEventTracker, useProject, useUser } from "@/hooks/store";
export const CyclesHeader: FC = observer(() => {
@@ -31,48 +30,52 @@ export const CyclesHeader: FC = observer(() => {
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
<div className="relative z-10 items-center justify-between gap-x-2 gap-y-4">
<div className="flex bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs onBack={router.back}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
href={`/${workspaceSlug}/projects/${currentProjectDetails?.id}/issues`}
icon={
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
)
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink label="Cycles" icon={<ContrastIcon className="h-4 w-4 text-custom-text-300" />} />
}
/>
</Breadcrumbs>
</div>
</div>
{canUserCreateCycle && (
<div className="flex items-center gap-3">
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
)}
</div>
{canUserCreateCycle && (
<div className="flex items-center gap-3">
<Button
variant="primary"
size="sm"
prependIcon={<Plus />}
onClick={() => {
setTrackElement("Cycles page");
toggleCreateCycleModal(true);
}}
>
<div className="hidden sm:block">Add</div> Cycle
</Button>
</div>
)}
</div>
);
});
+75 -29
View File
@@ -1,23 +1,35 @@
import { useCallback, useState } from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
// icons
import { PlusIcon } from "lucide-react";
// types
// hooks
import { List, PlusIcon, Sheet } from "lucide-react";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
// ui
import { Breadcrumbs, Button, LayersIcon } from "@plane/ui";
// components
import { Breadcrumbs, Button, LayersIcon, PhotoFilterIcon, Tooltip } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues";
// components
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace";
// ui
// icons
// types
// constants
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserWorkspaceRoles } from "@/constants/workspace";
// hooks
import { useLabel, useMember, useUser, useIssues } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const GlobalIssuesHeader: React.FC = observer(() => {
const GLOBAL_VIEW_LAYOUTS = [
{ key: "list", title: "List", link: "/workspace-views", icon: List },
{ key: "spreadsheet", title: "Spreadsheet", link: "/workspace-views/all-issues", icon: Sheet },
];
type Props = {
activeLayout: "list" | "spreadsheet";
};
export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const { activeLayout } = props;
// states
const [createViewModal, setCreateViewModal] = useState(false);
// router
@@ -34,6 +46,7 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
const {
workspace: { workspaceMemberIds },
} = useMember();
const { isMobile } = usePlatformOS();
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
@@ -103,32 +116,65 @@ export const GlobalIssuesHeader: React.FC = observer(() => {
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink label={`All Issues`} icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
<BreadcrumbLink
label={`All ${activeLayout === "spreadsheet" ? "Issues" : "Views"}`}
icon={
activeLayout === "spreadsheet" ? (
<LayersIcon className="h-4 w-4 text-custom-text-300" />
) : (
<PhotoFilterIcon className="h-4 w-4 text-custom-text-300" />
)
}
/>
}
/>
</Breadcrumbs>
</div>
<div className="flex items-center gap-2">
<>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
labels={workspaceLabels ?? undefined}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</>
<div className="flex items-center gap-1 rounded bg-custom-background-80 p-1">
{GLOBAL_VIEW_LAYOUTS.map((layout) => (
<Link key={layout.key} href={`/${workspaceSlug}/${layout.link}`}>
<span>
<Tooltip tooltipContent={layout.title} isMobile={isMobile}>
<div
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100 ${
activeLayout === layout.key ? "bg-custom-background-100 shadow-custom-shadow-2xs" : ""
}`}
>
<layout.icon
size={14}
strokeWidth={2}
className={`${activeLayout === layout.key ? "text-custom-text-100" : "text-custom-text-200"}`}
/>
</div>
</Tooltip>
</span>
</Link>
))}
</div>
{activeLayout === "spreadsheet" && (
<>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
labels={workspaceLabels ?? undefined}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</>
)}
{isAuthorizedUser && (
<Button variant="primary" size="sm" prependIcon={<PlusIcon />} onClick={() => setCreateViewModal(true)}>
New View
+162 -11
View File
@@ -1,21 +1,33 @@
import { useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// icons
import { Plus } from "lucide-react";
// ui
import { Breadcrumbs, Button, DiceIcon } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { ProjectLogo } from "@/components/project";
// constants
import { EUserProjectRoles } from "@/constants/project";
import { ListFilter, Plus, Search, X } from "lucide-react";
import { TModuleFilters } from "@plane/types";
// hooks
import { useApplication, useEventTracker, useProject, useUser } from "@/hooks/store";
import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui";
import { BreadcrumbLink } from "@/components/common";
import { FiltersDropdown } from "@/components/issues";
import { ModuleFiltersSelection, ModuleOrderByDropdown } from "@/components/modules";
import { ProjectLogo } from "@/components/project";
import { MODULE_VIEW_LAYOUTS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
import { cn } from "@/helpers/common.helper";
import { useApplication, useEventTracker, useMember, useModuleFilter, useProject, useUser } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
// components
// constants
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
// ui
// helpers
// types
export const ModulesListHeader: React.FC = observer(() => {
// refs
const inputRef = useRef<HTMLInputElement>(null);
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
// store hooks
const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker();
@@ -23,6 +35,54 @@ export const ModulesListHeader: React.FC = observer(() => {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const { isMobile } = usePlatformOS();
const {
workspace: { workspaceMemberIds },
} = useMember();
const {
currentProjectDisplayFilters: displayFilters,
currentProjectFilters: filters,
searchQuery,
updateDisplayFilters,
updateFilters,
updateSearchQuery,
} = useModuleFilter();
// states
const [isSearchOpen, setIsSearchOpen] = useState(searchQuery !== "" ? true : false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false);
});
const handleFilters = useCallback(
(key: keyof TModuleFilters, value: string | string[]) => {
if (!projectId) return;
const newValues = filters?.[key] ?? [];
if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
else {
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(projectId.toString(), { [key]: newValues });
},
[filters, projectId, updateFilters]
);
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (searchQuery && searchQuery.trim() !== "") updateSearchQuery("");
else {
setIsSearchOpen(false);
inputRef.current?.blur();
}
}
};
// auth
const canUserCreateModule =
@@ -57,6 +117,97 @@ export const ModulesListHeader: React.FC = observer(() => {
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center">
{!isSearchOpen && (
<button
type="button"
className="-mr-1 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={searchQuery}
onChange={(e) => updateSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
// updateSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<div className="hidden md:flex items-center gap-1 rounded bg-custom-background-80 p-1">
{MODULE_VIEW_LAYOUTS.map((layout) => (
<Tooltip key={layout.key} tooltipContent={layout.title} isMobile={isMobile}>
<button
type="button"
className={cn(
"group grid h-[22px] w-7 place-items-center overflow-hidden rounded transition-all hover:bg-custom-background-100",
{
"bg-custom-background-100 shadow-custom-shadow-2xs": displayFilters?.layout === layout.key,
}
)}
onClick={() => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), { layout: layout.key });
}}
>
<layout.icon
strokeWidth={2}
className={cn("h-3.5 w-3.5 text-custom-text-200", {
"text-custom-text-100": displayFilters?.layout === layout.key,
})}
/>
</button>
</Tooltip>
))}
</div>
<ModuleOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!projectId || val === displayFilters?.order_by) return;
updateDisplayFilters(projectId.toString(), {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ModuleFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleDisplayFiltersUpdate={(val) => {
if (!projectId) return;
updateDisplayFilters(projectId.toString(), val);
}}
handleFiltersUpdate={handleFilters}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
{canUserCreateModule && (
<Button
variant="primary"
+111 -108
View File
@@ -116,121 +116,124 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
<div className="relative z-[15] flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
<div className="relative z-[15] items-center gap-x-2 gap-y-4">
<div className="flex items-center gap-2 p-4 bg-custom-sidebar-background-100">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
href={`/${workspaceSlug}/projects`}
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<ProjectLogo logo={currentProjectDetails?.logo_props} className="text-sm" />
</span>
)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)
) : (
<span className="grid h-7 w-7 flex-shrink-0 place-items-center rounded uppercase">
<Briefcase className="h-4 w-4" />
</span>
)
}
/>
}
/>
}
/>
}
/>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink label="Issues" icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />} />
}
/>
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div>
{currentProjectDetails?.is_deployed && deployUrl && (
<a
href={`${deployUrl}/${workspaceSlug}/${currentProjectDetails?.id}`}
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
target="_blank"
rel="noopener noreferrer"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
Public
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
</a>
)}
</div>
{currentProjectDetails?.is_deployed && deployUrl && (
<a
href={`${deployUrl}/${workspaceSlug}/${currentProjectDetails?.id}`}
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
target="_blank"
rel="noopener noreferrer"
>
<Circle className="h-1.5 w-1.5 fill-custom-primary-100" strokeWidth={2} />
Public
<ExternalLink className="hidden h-3 w-3 group-hover:block" strokeWidth={2} />
</a>
<div className="items-center gap-2 hidden md:flex">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
{canUserCreateIssue && (
<>
<Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
Analytics
</Button>
<Button
onClick={() => {
setTrackElement("Project issues page");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
size="sm"
prependIcon={<Plus />}
>
<div className="hidden sm:block">Add</div> Issue
</Button>
</>
)}
</div>
<div className="items-center gap-2 hidden md:flex">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
{canUserCreateIssue && (
<>
<Button
className="hidden md:block"
onClick={() => setAnalyticsModal(true)}
variant="neutral-primary"
size="sm"
>
Analytics
</Button>
<Button
onClick={() => {
setTrackElement("Project issues page");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
size="sm"
prependIcon={<Plus />}
>
<div className="hidden sm:block">Add</div> Issue
</Button>
</>
)}
</div>
</>
);
+20 -22
View File
@@ -108,7 +108,7 @@ export const ProjectsHeader = observer(() => {
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-30 md:w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
@@ -135,29 +135,27 @@ export const ProjectsHeader = observer(() => {
)}
</div>
</div>
<div className="hidden md:flex gap-3">
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug, {
order_by: val,
});
<ProjectOrderByDropdown
value={displayFilters?.order_by}
onChange={(val) => {
if (!workspaceSlug || val === displayFilters?.order_by) return;
updateDisplayFilters(workspaceSlug, {
order_by: val,
});
}}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug, val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<ProjectFiltersSelection
displayFilters={displayFilters ?? {}}
filters={filters ?? {}}
handleFiltersUpdate={handleFilters}
handleDisplayFiltersUpdate={(val) => {
if (!workspaceSlug) return;
updateDisplayFilters(workspaceSlug, val);
}}
memberIds={workspaceMemberIds ?? undefined}
/>
</FiltersDropdown>
</div>
</FiltersDropdown>
{isAuthorizedUser && (
<Button
prependIcon={<Plus />}
+25 -4
View File
@@ -1,11 +1,13 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
import { Settings } from "lucide-react";
// ui
import { Breadcrumbs } from "@plane/ui";
import { Settings } from "lucide-react";
import { Breadcrumbs, CustomMenu } from "@plane/ui";
// hooks
// components
import { BreadcrumbLink } from "@/components/common";
import { WORKSPACE_SETTINGS_LINKS } from "@/constants/workspace";
export interface IWorkspaceSettingHeader {
title: string;
@@ -18,7 +20,7 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
const { workspaceSlug } = router.query;
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div>
<Breadcrumbs>
@@ -32,9 +34,28 @@ export const WorkspaceSettingHeader: FC<IWorkspaceSettingHeader> = observer((pro
/>
}
/>
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={title} />} />
<div className="hidden sm:hidden md:block lg:block">
<Breadcrumbs.BreadcrumbItem type="text" link={<BreadcrumbLink label={title} />} />
</div>
</Breadcrumbs>
</div>
<CustomMenu
className="flex-shrink-0 block sm:block md:hidden lg:hidden"
maxHeight="lg"
customButton={
<span className="text-xs px-1.5 py-1 border rounded-md text-custom-text-200 border-custom-border-300">
{title}
</span>
}
placement="bottom-start"
closeOnSelect
>
{WORKSPACE_SETTINGS_LINKS.map((item) => (
<CustomMenu.MenuItem key={item.key} onClick={() => router.push(`/${workspaceSlug}${item.href}`)}>
{item.label}
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
);
+23
View File
@@ -0,0 +1,23 @@
import type { Props } from "./types";
export const AlarmClockIcon: React.FC<Props> = ({
width = "24",
height = "24",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.98125 15.4609C8.05625 15.4609 7.18437 15.2859 6.36562 14.9359C5.54687 14.5859 4.83437 14.1078 4.22812 13.5016C3.62187 12.8953 3.14062 12.1828 2.78437 11.3641C2.42812 10.5453 2.25 9.66886 2.25 8.73469C2.25 7.80053 2.42812 6.92553 2.78437 6.10969C3.14062 5.29386 3.62187 4.57969 4.22812 3.96719C4.83437 3.35469 5.54687 2.87344 6.36562 2.52344C7.18437 2.17344 8.05625 1.99844 8.98125 1.99844C9.90625 1.99844 10.7781 2.17344 11.5969 2.52344C12.4156 2.87344 13.1312 3.35469 13.7437 3.96719C14.3562 4.57969 14.8375 5.29386 15.1875 6.10969C15.5375 6.92553 15.7125 7.80053 15.7125 8.73469C15.7125 9.66886 15.5375 10.5453 15.1875 11.3641C14.8375 12.1828 14.3562 12.8953 13.7437 13.5016C13.1312 14.1078 12.4156 14.5859 11.5969 14.9359C10.7781 15.2859 9.90625 15.4609 8.98125 15.4609ZM11.25 11.7859L12.0375 10.9984L9.6 8.56094V4.99844H8.475V9.01094L11.25 11.7859ZM4.0125 0.742188L4.8 1.52969L1.725 4.49219L0.9375 3.70469L4.0125 0.742188ZM13.95 0.742188L17.025 3.70469L16.2375 4.49219L13.1625 1.52969L13.95 0.742188ZM8.98206 14.3359C10.544 14.3359 11.8687 13.7919 12.9562 12.7039C14.0437 11.6158 14.5875 10.2908 14.5875 8.72888C14.5875 7.16692 14.0435 5.84219 12.9554 4.75469C11.8674 3.66719 10.5424 3.12344 8.98044 3.12344C7.41848 3.12344 6.09375 3.66746 5.00625 4.75549C3.91875 5.84353 3.375 7.16853 3.375 8.73049C3.375 10.2925 3.91902 11.6172 5.00706 12.7047C6.09509 13.7922 7.42009 14.3359 8.98206 14.3359Z"
fill="#F7AE59"
/>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
import React from "react";
import type { Props } from "./types";
export const ArchiveIcon: React.FC<Props> = ({ width = "24", height = "24", className, color }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.75 19.5C5.41667 19.5 5.125 19.375 4.875 19.125C4.625 18.875 4.5 18.5833 4.5 18.25V7.35417C4.5 7.14583 4.52083 6.96875 4.5625 6.82292C4.60417 6.67708 4.68056 6.54167 4.79167 6.41667L5.95833 4.83333C6.06944 4.70833 6.19792 4.62153 6.34375 4.57292C6.48958 4.52431 6.6624 4.5 6.86221 4.5H17.1378C17.3376 4.5 17.5069 4.52431 17.6458 4.57292C17.7847 4.62153 17.9097 4.70833 18.0208 4.83333L19.2083 6.41667C19.3194 6.54167 19.3958 6.67708 19.4375 6.82292C19.4792 6.96875 19.5 7.14583 19.5 7.35417V18.25C19.5 18.5833 19.375 18.875 19.125 19.125C18.875 19.375 18.5833 19.5 18.25 19.5H5.75ZM6.10417 6.70833H17.875L17.1165 5.75H6.85417L6.10417 6.70833ZM5.75 7.95833V18.25H18.25V7.95833H5.75ZM12 16.375L15.25 13.125L14.4167 12.2917L12.625 14.0833V9.89583H11.375V14.0833L9.58333 12.2917L8.75 13.125L12 16.375Z"
fill={color ? color : "currentColor"}
/>
</svg>
);
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const ArrowRightIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.9583 0.500103L15.0625 4.58344C15.1319 4.65288 15.1806 4.72232 15.2083 4.79177C15.2361 4.86121 15.25 4.9376 15.25 5.02094C15.25 5.10427 15.2361 5.18066 15.2083 5.2501C15.1806 5.31955 15.1319 5.38899 15.0625 5.45844L10.9583 9.5626C10.8472 9.67371 10.7014 9.73274 10.5208 9.73969C10.3403 9.74663 10.1875 9.6876 10.0625 9.5626C9.9375 9.4376 9.875 9.2883 9.875 9.11469C9.875 8.94108 9.9375 8.79177 10.0625 8.66677L13.0833 5.64594L1.125 5.64594C0.944443 5.64594 0.795138 5.58691 0.677083 5.46885C0.559026 5.3508 0.5 5.20149 0.5 5.02094C0.5 4.84038 0.559026 4.69108 0.677083 4.57302C0.795138 4.45496 0.944443 4.39594 1.125 4.39594L13.0833 4.39594L10.0625 1.3751C9.95139 1.26399 9.89236 1.12163 9.88542 0.94802C9.87847 0.774409 9.9375 0.625103 10.0625 0.500103C10.1875 0.375103 10.3368 0.312602 10.5104 0.312602C10.684 0.312602 10.8333 0.375103 10.9583 0.500103Z"
fill={color}
/>
</svg>
);
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const AssignmentClipboardIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill={color}
d="M2.125 19.25C1.74306 19.25 1.4184 19.1163 1.15104 18.8489C0.883681 18.5816 0.75 18.2569 0.75 17.875V4.12499C0.75 3.74305 0.883681 3.41839 1.15104 3.15103C1.4184 2.88367 1.74306 2.74999 2.125 2.74999H6.82292C6.89931 2.21527 7.14375 1.77603 7.55625 1.43228C7.96875 1.08853 8.45 0.916656 9 0.916656C9.55 0.916656 10.0312 1.08853 10.4438 1.43228C10.8562 1.77603 11.1007 2.21527 11.1771 2.74999H15.875C16.2569 2.74999 16.5816 2.88367 16.849 3.15103C17.1163 3.41839 17.25 3.74305 17.25 4.12499V17.875C17.25 18.2569 17.1163 18.5816 16.849 18.8489C16.5816 19.1163 16.2569 19.25 15.875 19.25H2.125ZM2.125 17.875H15.875V4.12499H2.125V17.875ZM4.41667 15.5833H10.6729V14.2083H4.41667V15.5833ZM4.41667 11.6875H13.5833V10.3125H4.41667V11.6875ZM4.41667 7.79166H13.5833V6.41666H4.41667V7.79166ZM9 3.73541C9.21389 3.73541 9.40104 3.6552 9.56146 3.49478C9.72188 3.33436 9.80208 3.14721 9.80208 2.93332C9.80208 2.71943 9.72188 2.53228 9.56146 2.37186C9.40104 2.21145 9.21389 2.13124 9 2.13124C8.78611 2.13124 8.59896 2.21145 8.43854 2.37186C8.27812 2.53228 8.19792 2.71943 8.19792 2.93332C8.19792 3.14721 8.27812 3.33436 8.43854 3.49478C8.59896 3.6552 8.78611 3.73541 9 3.73541ZM2.125 17.875V4.12499V17.875Z"
/>
</svg>
);
@@ -14,7 +14,7 @@ import {
SvgIcon,
TxtIcon,
VideoIcon,
} from "@/components/icons/attachment";
} from "@/components/icons";
export const getFileIcon = (fileType: string) => {
switch (fileType) {
-20
View File
@@ -1,20 +0,0 @@
export * from "./attachment-icon";
export * from "./audio-file-icon";
export * from "./css-file-icon";
export * from "./csv-file-icon";
export * from "./default-file-icon";
export * from "./doc-file-icon";
export * from "./document-icon";
export * from "./figma-file-icon";
export * from "./html-file-icon";
export * from "./img-file-icon";
export * from "./jpg-file-icon";
export * from "./js-file-icon";
export * from "./pdf-file-icon";
export * from "./png-file-icon";
export * from "./setting-icon";
export * from "./sheet-file-icon";
export * from "./svg-file-icon";
export * from "./tune-icon";
export * from "./txt-file-icon";
export * from "./video-file-icon";
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import type { Props } from "./types";
export const BellNotificationIcon: React.FC<Props> = ({
width = "24",
height = "24",
color = "rgb(var(--color-text-200))",
className,
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 22 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.35425 17.4154C4.15946 17.4154 3.99618 17.3491 3.8644 17.2166C3.73263 17.084 3.66675 16.9198 3.66675 16.7239C3.66675 16.5279 3.73263 16.365 3.8644 16.2352C3.99618 16.1053 4.15946 16.0404 4.35425 16.0404H5.59175V9.02786C5.59175 7.77509 5.96987 6.64071 6.72612 5.62474C7.48237 4.60877 8.47925 3.97092 9.71675 3.7112V3.04661C9.71675 2.69523 9.84099 2.40495 10.0895 2.17578C10.338 1.94661 10.6397 1.83203 10.9947 1.83203C11.3497 1.83203 11.6532 1.94661 11.9053 2.17578C12.1574 2.40495 12.2834 2.69523 12.2834 3.04661V3.7112C13.5209 3.97092 14.5216 4.60877 15.2855 5.62474C16.0494 6.64071 16.4313 7.77509 16.4313 9.02786V16.0404H17.6459C17.8407 16.0404 18.004 16.1066 18.1358 16.2392C18.2675 16.3717 18.3334 16.536 18.3334 16.7319C18.3334 16.9278 18.2675 17.0907 18.1358 17.2206C18.004 17.3504 17.8407 17.4154 17.6459 17.4154H4.35425ZM11.0001 20.1654C10.5112 20.1654 10.0834 19.9859 9.71675 19.6268C9.35008 19.2678 9.16675 18.8362 9.16675 18.332H12.8334C12.8334 18.8362 12.6539 19.2678 12.2949 19.6268C11.9358 19.9859 11.5042 20.1654 11.0001 20.1654ZM6.96675 16.0404H15.0563V9.02786C15.0563 7.88203 14.6706 6.91571 13.899 6.12891C13.1275 5.3421 12.1727 4.9487 11.0345 4.9487C9.89626 4.9487 8.93376 5.3421 8.14696 6.12891C7.36015 6.91571 6.96675 7.88203 6.96675 9.02786V16.0404Z"
fill={color}
/>
</svg>
);
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import type { Props } from "./types";
export const BlockedIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.5 4.8C0.5 3.52696 1.00797 2.30606 1.91216 1.40589C2.81636 0.505713 4.04271 0 5.32143 0H21.3929C21.6913 0 21.9839 0.0827435 22.2378 0.238959C22.4917 0.395174 22.6968 0.618689 22.8303 0.884458C22.9638 1.15023 23.0203 1.44775 22.9935 1.74369C22.9667 2.03963 22.8576 2.32229 22.6786 2.56L18.5804 8L22.6786 13.44C22.8576 13.6777 22.9667 13.9604 22.9935 14.2563C23.0203 14.5522 22.9638 14.8498 22.8303 15.1155C22.6968 15.3813 22.4917 15.6048 22.2378 15.761C21.9839 15.9173 21.6913 16 21.3929 16H5.32143C4.89519 16 4.4864 16.1686 4.18501 16.4686C3.88361 16.7687 3.71429 17.1757 3.71429 17.6V22.4C3.71429 22.8243 3.54496 23.2313 3.24356 23.5314C2.94217 23.8314 2.53338 24 2.10714 24C1.6809 24 1.27212 23.8314 0.970721 23.5314C0.669323 23.2313 0.5 22.8243 0.5 22.4V4.8Z"
fill="#F76659"
/>
<path
d="M8.5918 20.4812H21.084C21.26 20.4812 21.4056 20.4237 21.5207 20.3086C21.6358 20.1935 21.6934 20.0479 21.6934 19.8719C21.6934 19.6958 21.6358 19.5503 21.5207 19.4352C21.4056 19.3201 21.26 19.2625 21.084 19.2625H8.57148L10.3184 17.5156C10.4267 17.4073 10.4809 17.2719 10.4809 17.1094C10.4809 16.9469 10.4199 16.8047 10.298 16.6828C10.1762 16.5609 10.034 16.5 9.87148 16.5C9.70899 16.5 9.5668 16.5609 9.44492 16.6828L6.68242 19.4453C6.61471 19.513 6.56732 19.5807 6.54023 19.6484C6.51315 19.7161 6.49961 19.7906 6.49961 19.8719C6.49961 19.9531 6.51315 20.0276 6.54023 20.0953C6.56732 20.163 6.61471 20.2307 6.68242 20.2984L9.44492 23.0609C9.58034 23.1964 9.72591 23.2607 9.88164 23.2539C10.0374 23.2471 10.1762 23.1828 10.298 23.0609C10.4199 22.9391 10.4809 22.7935 10.4809 22.6242C10.4809 22.4549 10.4267 22.3161 10.3184 22.2078L8.5918 20.4812Z"
fill="#F76659"
/>
</svg>
);
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import type { Props } from "./types";
export const BlockerIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 23 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 4.8C0 3.52696 0.507971 2.30606 1.41216 1.40589C2.31636 0.505713 3.54271 0 4.82143 0H20.8929C21.1913 0 21.4839 0.0827435 21.7378 0.238959C21.9917 0.395174 22.1968 0.618689 22.3303 0.884458C22.4638 1.15023 22.5203 1.44775 22.4935 1.74369C22.4667 2.03963 22.3576 2.32229 22.1786 2.56L18.0804 8L22.1786 13.44C22.3576 13.6777 22.4667 13.9604 22.4935 14.2563C22.5203 14.5522 22.4638 14.8498 22.3303 15.1155C22.1968 15.3813 21.9917 15.6048 21.7378 15.761C21.4839 15.9173 21.1913 16 20.8929 16H4.82143C4.39519 16 3.9864 16.1686 3.68501 16.4686C3.38361 16.7687 3.21429 17.1757 3.21429 17.6V22.4C3.21429 22.8243 3.04496 23.2313 2.74356 23.5314C2.44217 23.8314 2.03338 24 1.60714 24C1.1809 24 0.772119 23.8314 0.470721 23.5314C0.169323 23.2313 0 22.8243 0 22.4V4.8Z"
fill="#F7AE59"
/>
<path
d="M18.5391 20.8797H6.04688C5.87083 20.8797 5.72526 20.8221 5.61016 20.707C5.49505 20.5919 5.4375 20.4464 5.4375 20.2703C5.4375 20.0943 5.49505 19.9487 5.61016 19.8336C5.72526 19.7185 5.87083 19.6609 6.04688 19.6609H18.5594L16.8125 17.9141C16.7042 17.8057 16.65 17.6703 16.65 17.5078C16.65 17.3453 16.7109 17.2031 16.8328 17.0813C16.9547 16.9594 17.0969 16.8984 17.2594 16.8984C17.4219 16.8984 17.5641 16.9594 17.6859 17.0813L20.4484 19.8438C20.5161 19.9115 20.5635 19.9792 20.5906 20.0469C20.6177 20.1146 20.6313 20.1891 20.6313 20.2703C20.6313 20.3516 20.6177 20.426 20.5906 20.4938C20.5635 20.5615 20.5161 20.6292 20.4484 20.6969L17.6859 23.4594C17.5505 23.5948 17.4049 23.6591 17.2492 23.6523C17.0935 23.6456 16.9547 23.5812 16.8328 23.4594C16.7109 23.3375 16.65 23.1919 16.65 23.0227C16.65 22.8534 16.7042 22.7146 16.8125 22.6062L18.5391 20.8797Z"
fill="#F7AE59"
/>
</svg>
);
+16
View File
@@ -0,0 +1,16 @@
import React from "react";
import type { Props } from "./types";
export const BoltIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.6002 21C10.4169 21 10.2752 20.9417 10.1752 20.825C10.0752 20.7083 10.0419 20.5583 10.0752 20.375L11.0002 13.95H7.3502C7.16686 13.95 7.03353 13.8667 6.9502 13.7C6.86686 13.5333 6.86686 13.375 6.9502 13.225L12.8752 3.325C12.9252 3.24167 13.0085 3.16667 13.1252 3.1C13.2419 3.03333 13.3585 3 13.4752 3C13.6585 3 13.8002 3.05833 13.9002 3.175C14.0002 3.29167 14.0335 3.44167 14.0002 3.625L13.0752 10.025H16.6752C16.8585 10.025 16.996 10.1083 17.0877 10.275C17.1794 10.4417 17.1835 10.6 17.1002 10.75L11.2002 20.675C11.1502 20.7583 11.0669 20.8333 10.9502 20.9C10.8335 20.9667 10.7169 21 10.6002 21V21Z" />
</svg>
);
@@ -0,0 +1,26 @@
import React from "react";
import type { Props } from "./types";
export const CalendarAfterIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3309_70901)">
<path
d="M10.6125 17V15.875H14.625V7.8125H3.375V11.9375H2.25V4.25C2.25 3.95 2.3625 3.6875 2.5875 3.4625C2.8125 3.2375 3.075 3.125 3.375 3.125H4.59375V2H5.8125V3.125H12.1875V2H13.4063V3.125H14.625C14.925 3.125 15.1875 3.2375 15.4125 3.4625C15.6375 3.6875 15.75 3.95 15.75 4.25V15.875C15.75 16.175 15.6375 16.4375 15.4125 16.6625C15.1875 16.8875 14.925 17 14.625 17H10.6125ZM6 18.2375L5.2125 17.45L7.33125 15.3125H0.9375V14.1875H7.33125L5.2125 12.05L6 11.2625L9.4875 14.75L6 18.2375ZM3.375 6.6875H14.625V4.25H3.375V6.6875Z"
fill="rgb(var(--color-text-200))"
/>
</g>
<defs>
<clipPath id="clip0_3309_70901">
<rect width="18" height="18" fill="white" transform="translate(0 0.5)" />
</clipPath>
</defs>
</svg>
);
@@ -0,0 +1,37 @@
import React from "react";
import type { Props } from "./types";
export const CalendarBeforeIcon: React.FC<Props> = ({ width = "24", height = "24", className }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3309_70907)">
<path
d="M10.6125 16.5V15.375H14.625V7.3125H3.375V11.4375H2.25V3.75C2.25 3.45 2.3625 3.1875 2.5875 2.9625C2.8125 2.7375 3.075 2.625 3.375 2.625H4.59375V1.5H5.8125V2.625H12.1875V1.5H13.4062V2.625H14.625C14.925 2.625 15.1875 2.7375 15.4125 2.9625C15.6375 3.1875 15.75 3.45 15.75 3.75V15.375C15.75 15.675 15.6375 15.9375 15.4125 16.1625C15.1875 16.3875 14.925 16.5 14.625 16.5H10.6125ZM3.375 6.1875H14.625V3.75H3.375V6.1875Z"
fill="rgb(var(--color-text-200))"
/>
<g clipPath="url(#clip1_3309_70907)">
<path
d="M3.99967 17.1667L1.33301 14.5L3.99967 11.8334L4.34967 12.1834L2.28301 14.25H8V14.75H2.28301L4.34967 16.8167L3.99967 17.1667Z"
fill="rgb(var(--color-text-200))"
stroke="rgb(var(--color-text-200))"
strokeWidth="0.5"
/>
</g>
</g>
<defs>
<clipPath id="clip0_3309_70907">
<rect width="18" height="18" fill="white" />
</clipPath>
<clipPath id="clip1_3309_70907">
<rect width="8" height="8" fill="white" transform="translate(0 10.5)" />
</clipPath>
</defs>
</svg>
);

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