Compare commits

...
Author SHA1 Message Date
a8140a5f08 chore: Add logger package for node server side apps (#6188)
* chore: Add logger as a package

* chore: Add logger package for node server side apps

* remove plane logger import in web

* resolve pr reviews and add client logger with readme update

* fix: transformation and added middleware for logging requests

* chore: update readme

* fix: env configurable max file size

---------

Co-authored-by: sriram veeraghanta <[email protected]>
2024-12-13 14:32:56 +05:30
Prateek ShouryaandGitHub 9234f21f26 [WEB-2848] improvement: enhanced components modularity (#6196)
* improvement: enhanced componenets modularity

* fix: lint errors resolved
2024-12-13 14:26:26 +05:30
Bavisetti NarayanandGitHub ab11e83535 [WEB-2843] chore: updated the cycle end date logic (#6194)
* chore: updated the cycle end date logic

* chore: changed the key for timezone
2024-12-13 13:34:07 +05:30
Akshita GoyalandGitHub b4112358ac [WEB-2688] chore: added icons and splitted issue header (#6195)
* chore: added icons and splitted issue header

* fix: added ee filler component

* fix: component name fixed

* fix: removed dupes

* fix: casing
2024-12-13 13:31:13 +05:30
Aaryan KhandelwalandGitHub 77239ebcd4 fix: GitHub casing across the platform (#6193) 2024-12-13 02:22:46 +05:30
Prateek ShouryaandGitHub 54f828cbfa refactor: enhance components modularity and introduce new UI componenets (#6192)
* feat: add navigation dropdown component

* chore: enhance title/ description loader and componenet modularity

* chore: issue store filter update

* chore: added few icons to ui package

* chore: improvements for tabs componenet

* chore: enhance sidebar modularity

* chore: update issue and router store to add support for additional issue layouts

* chore: enhanced cycle componenets modularity

* feat: added project grouping header for cycles list

* chore: enhanced project dropdown componenet by adding multiple selection functionality

* chore: enhanced rich text editor modularity by taking members ids as props for mentions

* chore: added functionality to filter disabled layouts in issue-layout dropdown

* chore: added support to pass project ids as props in project card list

* feat: multi select project modal

* chore: seperate out project componenet for reusability

* chore: command pallete store improvements

* fix: build errors
2024-12-12 21:40:57 +05:30
Bavisetti NarayanandGitHub 9ad8b43408 chore: handled the cycle date time using project timezone (#6187)
* chore: handled the cycle date time using project timezone

* chore: reverted the frontend commit
2024-12-12 14:11:12 +05:30
Prateek ShouryaandGitHub 38e8a5c807 fix: command palette build (#6186) 2024-12-11 18:19:09 +05:30
Prateek ShouryaandGitHub a9bd2e243a refactor: enhance command palette modularity (#6139)
* refactor: enhance command palette modularity

* chore: minor updates to command palette store
2024-12-11 18:02:58 +05:30
Vamsi KrishnaandGitHub ca0d50b229 fix: no activity while moving inbox issues (#6185) 2024-12-11 17:57:27 +05:30
Vamsi KrishnaGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
7fca7fd86c [WEB-2774] fix:favorites reorder (#6179)
* fix:favorites reorder

* chore: added error handling

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-12-11 16:29:39 +05:30
Prateek ShouryaandGitHub 0ac68f2731 improvement: refactored issue grouping logic to access MobX store directly (#6134)
* improvement: refactored issue grouping logic to access MobX store directly

* chore: minor updates
2024-12-11 15:14:15 +05:30
5a9ae66680 chore: Remove shouldIgnoreDependencies flags while dragging in timeline view (#6150)
* remove shouldEnable dependency flags for timeline view

* chore: error handling

---------

Co-authored-by: Prateek Shourya <[email protected]>
2024-12-11 13:43:48 +05:30
Vamsi KrishnaandGitHub 134644fdf1 [WEB-2382]chore:notification files restructuring (#6181)
* chore: adjusted  increment/decrement  for unread count

* chore: improved param handling for unread notification count function

* chore:file restructuring

* fix:notification types

* chore:file restructuring

* chore:modified notfication types

* chore: modified types for notification

* chore:removed redundant checks for id
2024-12-11 13:41:19 +05:30
sriram veeraghanta d0f3987aeb fix: instance changelog url updated 2024-12-10 21:03:44 +05:30
sriram veeraghanta f06b1b8c4a fix: updated package version 2024-12-10 21:02:29 +05:30
sriram veeraghanta 6e56ea4c60 fix: updated changelog url in apiserver 2024-12-10 20:28:51 +05:30
198 changed files with 4376 additions and 2775 deletions
+3 -3
View File
@@ -44,7 +44,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
loading: "Saving Configuration...",
success: {
title: "Configuration saved",
message: () => `Github authentication is now ${value ? "active" : "disabled"}.`,
message: () => `GitHub authentication is now ${value ? "active" : "disabled"}.`,
},
error: {
title: "Error",
@@ -67,8 +67,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<AuthenticationMethodCard
name="Github"
description="Allow members to login or sign up to plane with their Github accounts."
name="GitHub"
description="Allow members to login or sign up to plane with their GitHub accounts."
icon={
<Image
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
+1 -1
View File
@@ -30,7 +30,7 @@ export const InstanceHeader: FC = observer(() => {
case "google":
return "Google";
case "github":
return "Github";
return "GitHub";
case "gitlab":
return "GitLab";
case "workspace":
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "admin",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
+1 -1
View File
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
WORKDIR /code
+1 -1
View File
@@ -4,7 +4,7 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
RUN apk --no-cache add \
"bash~=5.2" \
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.24.0"
"version": "0.24.1"
}
+13 -1
View File
@@ -4,7 +4,7 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from plane.db.models import Cycle, CycleIssue
from plane.utils.timezone_converter import convert_to_utc
class CycleSerializer(BaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
@@ -24,6 +24,18 @@ class CycleSerializer(BaseSerializer):
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
)
return data
class Meta:
+12
View File
@@ -5,6 +5,7 @@ from rest_framework import serializers
from .base import BaseSerializer
from .issue import IssueStateSerializer
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
from plane.utils.timezone_converter import convert_to_utc
class CycleWriteSerializer(BaseSerializer):
@@ -15,6 +16,17 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
if (
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
)
return data
class Meta:
+49 -5
View File
@@ -1,5 +1,7 @@
# Python imports
import json
import pytz
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
@@ -52,6 +54,11 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
# Module imports
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.webhook_task import model_activity
from plane.utils.timezone_converter import (
convert_utc_to_project_timezone,
convert_to_utc,
user_timezone_converter,
)
class CycleViewSet(BaseViewSet):
@@ -67,6 +74,19 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
project = Project.objects.get(id=self.kwargs.get("project_id"))
# Fetch project for the specific record or pass project_id dynamically
project_timezone = project.timezone
# Convert the current time (timezone.now()) to the project's timezone
local_tz = pytz.timezone(project_timezone)
current_time_in_project_tz = timezone.now().astimezone(local_tz)
# Convert project local time back to UTC for comparison (start_date is stored in UTC)
current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc)
return self.filter_queryset(
super()
.get_queryset()
@@ -119,12 +139,15 @@ class CycleViewSet(BaseViewSet):
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now())
& Q(end_date__gte=timezone.now()),
Q(start_date__lte=current_time_in_utc)
& Q(end_date__gte=current_time_in_utc),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
start_date__gt=current_time_in_utc,
then=Value("UPCOMING"),
),
When(end_date__lt=current_time_in_utc, then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT"),
@@ -160,10 +183,22 @@ class CycleViewSet(BaseViewSet):
# Update the order by
queryset = queryset.order_by("-is_favorite", "-created_at")
project = Project.objects.get(id=self.kwargs.get("project_id"))
# Fetch project for the specific record or pass project_id dynamically
project_timezone = project.timezone
# Convert the current time (timezone.now()) to the project's timezone
local_tz = pytz.timezone(project_timezone)
current_time_in_project_tz = timezone.now().astimezone(local_tz)
# Convert project local time back to UTC for comparison (start_date is stored in UTC)
current_time_in_utc = current_time_in_project_tz.astimezone(pytz.utc)
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(), end_date__gte=timezone.now()
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
)
data = queryset.values(
@@ -191,6 +226,8 @@ class CycleViewSet(BaseViewSet):
"version",
"created_by",
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, project_timezone)
if data:
return Response(data, status=status.HTTP_200_OK)
@@ -221,6 +258,8 @@ class CycleViewSet(BaseViewSet):
"version",
"created_by",
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@@ -417,6 +456,8 @@ class CycleViewSet(BaseViewSet):
)
queryset = queryset.first()
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
recent_visited_task.delay(
slug=slug,
@@ -492,6 +533,9 @@ class CycleDateCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
start_date = convert_to_utc(str(start_date), project_id, is_start_date=True)
end_date = convert_to_utc(str(end_date), project_id)
# Check if any cycle intersects in the given interval
cycles = Cycle.objects.filter(
Q(workspace__slug=slug)
+1 -1
View File
@@ -54,7 +54,7 @@ from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from .. import BaseAPIView, BaseViewSet
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.utils.timezone_converter import user_timezone_converter
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.global_paginator import paginate
from plane.bgtasks.webhook_task import model_activity
+1 -1
View File
@@ -20,7 +20,7 @@ from plane.app.serializers import IssueSerializer
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue
from plane.bgtasks.issue_activities_task import issue_activity
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.utils.timezone_converter import user_timezone_converter
from collections import defaultdict
+1 -1
View File
@@ -28,7 +28,7 @@ from plane.app.permissions import ProjectEntityPermission
from plane.app.serializers import ModuleDetailSerializer
from plane.db.models import Issue, Module, ModuleLink, UserFavorite, Project
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.utils.timezone_converter import user_timezone_converter
# Module imports
+1 -1
View File
@@ -56,7 +56,7 @@ from plane.db.models import (
Project,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.utils.timezone_converter import user_timezone_converter
from plane.bgtasks.webhook_task import model_activity
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.recent_visited_task import recent_visited_task
+1 -1
View File
@@ -10,7 +10,7 @@ from plane.app.views.base import BaseAPIView
from plane.db.models import Cycle
from plane.app.permissions import WorkspaceViewerPermission
from plane.app.serializers.cycle import CycleSerializer
from plane.utils.timezone_converter import user_timezone_converter
class WorkspaceCyclesEndpoint(BaseAPIView):
permission_classes = [WorkspaceViewerPermission]
+100
View File
@@ -0,0 +1,100 @@
import pytz
from plane.db.models import Project
from datetime import datetime, time
from datetime import timedelta
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
user_tz = pytz.timezone(user_timezone)
# Check if queryset is a dictionary (single item) or a list of dictionaries
if isinstance(queryset, dict):
queryset_values = [queryset]
else:
queryset_values = list(queryset)
# Iterate over the dictionaries in the list
for item in queryset_values:
# Iterate over the datetime fields
for field in datetime_fields:
# Convert the datetime field to the user's timezone
if field in item and item[field]:
item[field] = item[field].astimezone(user_tz)
# If queryset was a single item, return a single item
if isinstance(queryset, dict):
return queryset_values[0]
else:
return queryset_values
def convert_to_utc(date, project_id, is_start_date=False):
"""
Converts a start date string to the project's local timezone at 12:00 AM
and then converts it to UTC for storage.
Args:
date (str): The date string in "YYYY-MM-DD" format.
project_id (int): The project's ID to fetch the associated timezone.
Returns:
datetime: The UTC datetime.
"""
# Retrieve the project's timezone using the project ID
project = Project.objects.get(id=project_id)
project_timezone = project.timezone
if not date or not project_timezone:
raise ValueError("Both date and timezone must be provided.")
# Parse the string into a date object
start_date = datetime.strptime(date, "%Y-%m-%d").date()
# Get the project's timezone
local_tz = pytz.timezone(project_timezone)
# Combine the date with 12:00 AM time
local_datetime = datetime.combine(start_date, time.min)
# Localize the datetime to the project's timezone
localized_datetime = local_tz.localize(local_datetime)
# If it's an start date, add one minute
if is_start_date:
localized_datetime += timedelta(minutes=1)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
# Return the UTC datetime for storage
return utc_datetime
def convert_utc_to_project_timezone(utc_datetime, project_id):
"""
Converts a UTC datetime (stored in the database) to the project's local timezone.
Args:
utc_datetime (datetime): The UTC datetime to be converted.
project_id (int): The project's ID to fetch the associated timezone.
Returns:
datetime: The datetime in the project's local timezone.
"""
# Retrieve the project's timezone using the project ID
project = Project.objects.get(id=project_id)
project_timezone = project.timezone
if not project_timezone:
raise ValueError("Project timezone must be provided.")
# Get the timezone object for the project's timezone
local_tz = pytz.timezone(project_timezone)
# Convert the UTC datetime to the project's local timezone
if utc_datetime.tzinfo is None:
# Localize UTC datetime if it's naive (i.e., without timezone info)
utc_datetime = pytz.utc.localize(utc_datetime)
# Convert to the project's local timezone
local_datetime = utc_datetime.astimezone(local_tz)
return local_datetime
@@ -1,26 +0,0 @@
import pytz
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
user_tz = pytz.timezone(user_timezone)
# Check if queryset is a dictionary (single item) or a list of dictionaries
if isinstance(queryset, dict):
queryset_values = [queryset]
else:
queryset_values = list(queryset)
# Iterate over the dictionaries in the list
for item in queryset_values:
# Iterate over the datetime fields
for field in datetime_fields:
# Convert the datetime field to the user's timezone
if field in item and item[field]:
item[field] = item[field].astimezone(user_tz)
# If queryset was a single item, return a single item
if isinstance(queryset, dict):
return queryset_values[0]
else:
return queryset_values
+1 -1
View File
@@ -70,7 +70,7 @@
"value": ""
},
"GITHUB_CLIENT_SECRET": {
"description": "Github Client Secret",
"description": "GitHub Client Secret",
"value": ""
},
"NEXT_PUBLIC_API_BASE_URL": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.24.0",
"version": "0.24.1",
"description": "",
"main": "./src/server.ts",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"repository": "https://github.com/makeplane/plane.git",
"version": "0.24.0",
"version": "0.24.1",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"main": "./src/index.ts"
}
+1
View File
@@ -11,6 +11,7 @@ export enum EIssueGroupByToServerOptions {
"target_date" = "target_date",
"project" = "project_id",
"created_by" = "created_by",
"team_project" = "project_id",
}
export enum EIssueGroupBYServerToProperty {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.24.0",
"version": "0.24.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.24.0",
"version": "0.24.1",
"files": [
"library.js",
"next.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.24.0",
"version": "0.24.1",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
+9
View File
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+59
View File
@@ -0,0 +1,59 @@
# Logger Package
This package provides a logger and a request logger utility built using [Winston](https://github.com/winstonjs/winston). It offers customizable log levels using env and supports structured logging for general application logs and HTTP requests.
## Features.
- Dynamic log level configuration using env.
- Pre-configured winston logger for general usage (`logger`).
- Request logger middleware that logs incoming request
## Usage
### Adding as a package
Add this package as a dependency in package.json
```typescript
dependency: {
...
@plane/logger":"*",
...
}
```
### Importing the Logger
```typescript
import { logger, requestLogger } from '@plane/logger'
```
### Usage
### `logger`: General Logger
Use this for general application logs.
```typescript
logger.info("This is an info log");
logger.warn("This is a warning");
logger.error("This is an error");
```
### `requestLogger`: Request Logger Middleware
Use this as a middleware for incoming requests
```typescript
const app = express()
app.use(requestLogger)
```
## Available Log Levels
- `error`
- `warn`
- `info` (default)
- `http`
- `verbose`
- `debug`
- `silly`
## Log file
- Log files are stored in logs folder of current working directory. Error logs are stored in files with format `error-%DATE%.log` and combined logs are stored with format `combined-%DATE%.log`.
- Log files have a 7 day rotation period defined.
## Configuration
- By default, the log level is set to `info`.
- You can specify a log level by adding a LOG_LEVEL in .env.
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@plane/logger",
"version": "0.24.1",
"description": "Logger shared across multiple apps internally",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
}
+66
View File
@@ -0,0 +1,66 @@
import winston from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
import path from "path";
// Define log levels
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
// Define colors for each level
const colors = {
error: "red",
warn: "yellow",
info: "green",
http: "magenta",
debug: "white",
};
// Tell winston about our colors
winston.addColors(colors);
// Custom format for logging
const format = winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info: winston.Logform.TransformableInfo) => `[${info?.timestamp}] ${info.level}: ${info.message}`
)
);
// Define which transports to use
const transports = [
// Console transport
new winston.transports.Console(),
// Rotating file transport for errors
new DailyRotateFile({
filename: path.join(process.cwd(), "logs", "error-%DATE%.log"),
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: process.env.LOG_MAX_SIZE || "20m",
maxFiles: process.env.LOG_RETENTION || "7d",
level: "error",
}),
// Rotating file transport for all logs
new DailyRotateFile({
filename: path.join(process.cwd(), "logs", "combined-%DATE%.log"),
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: process.env.LOG_MAX_SIZE || "20m",
maxFiles: process.env.LOG_RETENTION || "7d",
}),
];
// Create the logger
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
levels,
format,
transports,
});
+2
View File
@@ -0,0 +1,2 @@
export * from "./config";
export * from "./middleware";
+23
View File
@@ -0,0 +1,23 @@
import { Request, Response, NextFunction } from "express";
import { logger } from "./config";
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
// Log when the request starts
const startTime = Date.now();
// Log request details
logger.http(`Incoming ${req.method} request to ${req.url} from ${req.ip}`);
// Log request body if present
if (Object.keys(req.body).length > 0) {
logger.debug("Request body:", req.body);
}
// Capture response
res.on("finish", () => {
const duration = Date.now() - startTime;
logger.http(`Completed ${req.method} ${req.url} with status ${res.statusCode} in ${duration}ms`);
});
next();
};
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "@plane/typescript-config/base.json",
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"experimentalDecorators": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tailwind-config-custom",
"version": "0.24.0",
"version": "0.24.1",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"types": "./src/index.d.ts",
"main": "./src/index.d.ts"
+15
View File
@@ -0,0 +1,15 @@
export type TCommandPaletteActionList = Record<
string,
{ title: string; description: string; action: () => void }
>;
export type TCommandPaletteShortcutList = {
key: string;
title: string;
shortcuts: TCommandPaletteShortcut[];
};
export type TCommandPaletteShortcut = {
keys: string; // comma separated keys
description: string;
};
+2
View File
@@ -22,3 +22,5 @@ export type TLogoProps = {
background_color?: string;
};
};
export type TNameDescriptionLoader = "submitting" | "submitted" | "saved";
+1
View File
@@ -59,4 +59,5 @@ export enum EFileAssetType {
USER_AVATAR = "USER_AVATAR",
USER_COVER = "USER_COVER",
WORKSPACE_LOGO = "WORKSPACE_LOGO",
TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION",
}
+1
View File
@@ -32,3 +32,4 @@ export * from "./workspace-notifications";
export * from "./favorite";
export * from "./file";
export * from "./workspace-draft-issues/base";
export * from "./command-palette";
+3 -2
View File
@@ -211,12 +211,13 @@ export type GroupByColumnTypes =
| "priority"
| "labels"
| "assignees"
| "created_by";
| "created_by"
| "team_project";
export interface IGroupByColumn {
id: string;
name: string;
icon: ReactElement | undefined;
icon?: ReactElement | undefined;
payload: Partial<TIssue>;
isDropDisabled?: boolean;
dropErrorMessage?: string;
+1
View File
@@ -1,2 +1,3 @@
export * from "./project_filters";
export * from "./projects";
export * from "./project_link";
+22
View File
@@ -0,0 +1,22 @@
export type TProjectLinkEditableFields = {
title: string;
url: string;
};
export type TProjectLink = TProjectLinkEditableFields & {
created_by_id: string;
id: string;
metadata: any;
project_id: string;
//need
created_at: Date;
};
export type TProjectLinkMap = {
[project_id: string]: TProjectLink;
};
export type TProjectLinkIdMap = {
[project_id: string]: string[];
};
+3
View File
@@ -18,6 +18,7 @@ export type TIssueGroupByOptions =
| "cycle"
| "module"
| "target_date"
| "team_project"
| null;
export type TIssueOrderByOptions =
@@ -69,6 +70,7 @@ export type TIssueParams =
| "start_date"
| "target_date"
| "project"
| "team_project"
| "group_by"
| "sub_group_by"
| "order_by"
@@ -92,6 +94,7 @@ export interface IIssueFilterOptions {
cycle?: string[] | null;
module?: string[] | null;
project?: string[] | null;
team_project?: string[] | null;
start_date?: string[] | null;
state?: string[] | null;
state_group?: string[] | null;
+1 -1
View File
@@ -35,7 +35,7 @@ export type TNotificationData = {
};
export type TNotification = {
id: string | undefined;
id: string;
title: string | undefined;
data: TNotificationData | undefined;
entity_identifier: string | undefined;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"files": [
"base.json",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.24.0",
"version": "0.24.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+1
View File
@@ -1 +1,2 @@
export * from "./breadcrumbs";
export * from "./navigation-dropdown";
@@ -0,0 +1,96 @@
"use client";
import * as React from "react";
import { CheckIcon, ChevronDownIcon } from "lucide-react";
// ui
import { CustomMenu, TContextMenuItem } from "../dropdowns";
// helpers
import { cn } from "../../helpers";
type TBreadcrumbNavigationDropdownProps = {
selectedItemKey: string;
navigationItems: TContextMenuItem[];
navigationDisabled?: boolean;
};
export const BreadcrumbNavigationDropdown = (props: TBreadcrumbNavigationDropdownProps) => {
const { selectedItemKey, navigationItems, navigationDisabled = false } = props;
// derived values
const selectedItem = navigationItems.find((item) => item.key === selectedItemKey);
const selectedItemIcon = selectedItem?.icon ? (
<selectedItem.icon className={cn("size-3.5", selectedItem.iconClassName)} />
) : undefined;
// if no selected item, return null
if (!selectedItem) return null;
const NavigationButton = ({ className }: { className?: string }) => (
<li
className={cn(
"flex items-center justify-center cursor-default text-sm font-medium text-custom-text-200 group-hover:text-custom-text-100 outline-none",
className
)}
tabIndex={-1}
>
{selectedItemIcon && (
<div className="flex h-5 w-5 items-center justify-start overflow-hidden">{selectedItemIcon}</div>
)}
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{selectedItem.title}</div>
</li>
);
if (navigationDisabled) {
return <NavigationButton />;
}
return (
<CustomMenu
customButton={
<div className="group flex items-center gap-1.5">
<NavigationButton className="cursor-pointer" />
<ChevronDownIcon className="size-4 text-custom-text-200 group-hover:text-custom-text-100" />
</div>
}
placement="bottom-start"
closeOnSelect
>
{navigationItems.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (item.key === selectedItemKey) return;
item.action();
}}
className={cn(
"flex items-center gap-2",
{
"text-custom-text-400": item.disabled,
},
item.className
)}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("size-3.5", item.iconClassName)} />}
<div className="w-full">
<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>
{item.key === selectedItemKey && <CheckIcon className="flex-shrink-0 size-3.5" />}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
);
};
+22
View File
@@ -0,0 +1,22 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const ActivityIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_15681_9387)">
<path
d="M14.6666 8.00001H13.0133C12.7219 7.99939 12.4384 8.09421 12.206 8.26999C11.9736 8.44576 11.8053 8.69281 11.7266 8.97334L10.1599 14.5467C10.1498 14.5813 10.1288 14.6117 10.0999 14.6333C10.0711 14.655 10.036 14.6667 9.99992 14.6667C9.96386 14.6667 9.92877 14.655 9.89992 14.6333C9.87107 14.6117 9.85002 14.5813 9.83992 14.5467L6.15992 1.45334C6.14982 1.41872 6.12877 1.38831 6.09992 1.36668C6.07107 1.34504 6.03598 1.33334 5.99992 1.33334C5.96386 1.33334 5.92877 1.34504 5.89992 1.36668C5.87107 1.38831 5.85002 1.41872 5.83992 1.45334L4.27325 7.02668C4.1949 7.30611 4.02751 7.55235 3.79649 7.72802C3.56548 7.90368 3.28347 7.99918 2.99325 8.00001H1.33325"
stroke="#8591AD"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_15681_9387">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const AtRiskIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16" }) => (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
fill="#CC7700"
/>
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
stroke="#F3F4F7"
stroke-width="2"
/>
<g clip-path="url(#clip0_21157_25600)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.0369 15.3346H10.667C11.0352 15.3346 11.3337 15.6331 11.3337 16.0013C11.3337 16.3695 11.0352 16.668 10.667 16.668H10.0369C10.3686 19.6679 12.912 22.0013 16.0003 22.0013C19.0887 22.0013 21.6321 19.6679 21.9637 16.668H21.3337C20.9655 16.668 20.667 16.3695 20.667 16.0013C20.667 15.6331 20.9655 15.3346 21.3337 15.3346H21.9637C21.6321 12.3347 19.0887 10.0013 16.0003 10.0013C12.912 10.0013 10.3686 12.3347 10.0369 15.3346ZM8.66699 16.0013C8.66699 11.9512 11.9502 8.66797 16.0003 8.66797C20.0504 8.66797 23.3337 11.9512 23.3337 16.0013C23.3337 20.0514 20.0504 23.3346 16.0003 23.3346C11.9502 23.3346 8.66699 20.0514 8.66699 16.0013ZM16.0003 12.668C16.3685 12.668 16.667 12.9664 16.667 13.3346V16.0013C16.667 16.3695 16.3685 16.668 16.0003 16.668C15.6321 16.668 15.3337 16.3695 15.3337 16.0013V13.3346C15.3337 12.9664 15.6321 12.668 16.0003 12.668ZM15.3337 18.668C15.3337 18.2998 15.6321 18.0013 16.0003 18.0013H16.007C16.3752 18.0013 16.6737 18.2998 16.6737 18.668C16.6737 19.0362 16.3752 19.3346 16.007 19.3346H16.0003C15.6321 19.3346 15.3337 19.0362 15.3337 18.668Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_21157_25600">
<rect width="16" height="16" fill="white" transform="translate(8 8)" />
</clipPath>
</defs>
</svg>
);
+9
View File
@@ -16,6 +16,7 @@ export * from "./epic-icon";
export * from "./full-screen-panel-icon";
export * from "./github-icon";
export * from "./gitlab-icon";
export * from "./info-fill-icon";
export * from "./info-icon";
export * from "./layer-stack";
export * from "./layers-icon";
@@ -38,3 +39,11 @@ export * from "./done-icon";
export * from "./pending-icon";
export * from "./pi-chat";
export * from "./workspace-icon";
export * from "./teams";
export * from "./lead-icon";
export * from "./activity-icon";
export * from "./updates-icon";
export * from "./overview-icon";
export * from "./on-track-icon";
export * from "./off-track-icon";
export * from "./at-risk-icon";
+26
View File
@@ -0,0 +1,26 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const LeadIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg className={className} viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...rest}>
<path
d="M0.571533 9C0.571533 4.02944 4.60097 0 9.57153 0C14.5421 0 18.5715 4.02944 18.5715 9C18.5715 13.9706 14.5421 18 9.57153 18C4.60097 18 0.571533 13.9706 0.571533 9Z"
fill="#3372FF"
/>
<g clip-path="url(#clip0_8992_2377)">
<circle cx="9.57153" cy="6.5" r="2.5" fill="#F5F5FF" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.94653 9.625C6.53029 9.625 4.57153 11.5838 4.57153 14H9.57153H14.5715C14.5715 11.5838 12.6128 9.625 10.1965 9.625H9.82153L10.8215 13.0278L9.57153 14L8.32153 13.0278L9.32153 9.625H8.94653Z"
fill="#F5F5FF"
/>
</g>
<defs>
<clipPath id="clip0_8992_2377">
<rect width="10" height="10" fill="white" transform="translate(4.57153 4)" />
</clipPath>
</defs>
</svg>
);
+30
View File
@@ -0,0 +1,30 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const OffTrackIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16" }) => (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
fill="#CC0000"
/>
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
stroke="#F3F4F7"
stroke-width="2"
/>
<g clip-path="url(#clip0_21157_78200)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.0369 15.3346H10.667C11.0352 15.3346 11.3337 15.6331 11.3337 16.0013C11.3337 16.3695 11.0352 16.668 10.667 16.668H10.0369C10.3686 19.6679 12.912 22.0013 16.0003 22.0013C19.0887 22.0013 21.6321 19.6679 21.9637 16.668H21.3337C20.9655 16.668 20.667 16.3695 20.667 16.0013C20.667 15.6331 20.9655 15.3346 21.3337 15.3346H21.9637C21.6321 12.3347 19.0887 10.0013 16.0003 10.0013C12.912 10.0013 10.3686 12.3347 10.0369 15.3346ZM8.66699 16.0013C8.66699 11.9512 11.9502 8.66797 16.0003 8.66797C20.0504 8.66797 23.3337 11.9512 23.3337 16.0013C23.3337 20.0514 20.0504 23.3346 16.0003 23.3346C11.9502 23.3346 8.66699 20.0514 8.66699 16.0013ZM14.667 12.668C15.0352 12.668 15.3337 12.9664 15.3337 13.3346V16.0013C15.3337 16.3695 15.0352 16.668 14.667 16.668C14.2988 16.668 14.0003 16.3695 14.0003 16.0013V13.3346C14.0003 12.9664 14.2988 12.668 14.667 12.668ZM17.3337 12.668C17.7018 12.668 18.0003 12.9664 18.0003 13.3346V16.0013C18.0003 16.3695 17.7018 16.668 17.3337 16.668C16.9655 16.668 16.667 16.3695 16.667 16.0013V13.3346C16.667 12.9664 16.9655 12.668 17.3337 12.668ZM14.0003 18.668C14.0003 18.2998 14.2988 18.0013 14.667 18.0013H14.6737C15.0418 18.0013 15.3403 18.2998 15.3403 18.668C15.3403 19.0362 15.0418 19.3346 14.6737 19.3346H14.667C14.2988 19.3346 14.0003 19.0362 14.0003 18.668ZM16.667 18.668C16.667 18.2998 16.9655 18.0013 17.3337 18.0013H17.3403C17.7085 18.0013 18.007 18.2998 18.007 18.668C18.007 19.0362 17.7085 19.3346 17.3403 19.3346H17.3337C16.9655 19.3346 16.667 19.0362 16.667 18.668Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_21157_78200">
<rect width="16" height="16" fill="white" transform="translate(8 8)" />
</clipPath>
</defs>
</svg>
);
+48
View File
@@ -0,0 +1,48 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const OnTrackIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16" }) => (
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
fill="#1FAD40"
/>
<path
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1Z"
stroke="#F3F4F7"
stroke-width="2"
/>
<g clip-path="url(#clip0_21157_107468)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M19.0005 10.8004C17.8118 10.1146 16.4238 9.85693 15.0681 10.0705C13.7124 10.2841 12.4709 10.956 11.5507 11.9741C10.6304 12.9923 10.087 14.2952 10.0111 15.6655C9.93516 17.0358 10.3313 18.3907 11.1334 19.5043C11.9356 20.6179 13.0953 21.4228 14.4191 21.7849C15.7429 22.1469 17.1508 22.0442 18.408 21.4938C19.6652 20.9435 20.6958 19.9787 21.3278 18.7605C21.9598 17.5423 22.1551 16.1442 21.8811 14.7994C21.8076 14.4387 22.0404 14.0866 22.4012 14.0131C22.762 13.9396 23.1141 14.1725 23.1876 14.5332C23.5225 16.1768 23.2838 17.8856 22.5113 19.3745C21.7389 20.8635 20.4793 22.0426 18.9427 22.7153C17.4061 23.3879 15.6853 23.5135 14.0673 23.071C12.4493 22.6285 11.032 21.6447 10.0516 20.2836C9.07117 18.9226 8.58699 17.2665 8.67979 15.5917C8.77259 13.9169 9.43675 12.3245 10.5615 11.0801C11.6863 9.83568 13.2037 9.01448 14.8606 8.75343C16.5176 8.49238 18.2139 8.80726 19.6668 9.64556C19.9857 9.82957 20.0951 10.2373 19.9111 10.5562C19.7271 10.8751 19.3194 10.9845 19.0005 10.8004ZM23.1384 10.1949C23.3987 10.4553 23.3987 10.8774 23.1384 11.1377L16.4717 17.8044C16.2114 18.0648 15.7893 18.0648 15.5289 17.8044L13.5289 15.8044C13.2686 15.5441 13.2686 15.1219 13.5289 14.8616C13.7893 14.6012 14.2114 14.6012 14.4717 14.8616L16.0003 16.3902L22.1956 10.1949C22.4559 9.93458 22.878 9.93458 23.1384 10.1949Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.0003 23.333C15.6321 23.333 15.3337 23.0345 15.3337 22.6663V21.333C15.3337 20.9648 15.6321 20.6663 16.0003 20.6663C16.3685 20.6663 16.667 20.9648 16.667 21.333V22.6663C16.667 23.0345 16.3685 23.333 16.0003 23.333Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.0003 11.333C15.6321 11.333 15.3337 11.0345 15.3337 10.6663V9.333C15.3337 8.96481 15.6321 8.66634 16.0003 8.66634C16.3685 8.66634 16.667 8.96481 16.667 9.333V10.6663C16.667 11.0345 16.3685 11.333 16.0003 11.333Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.3337 15.9997C11.3337 16.3679 11.0352 16.6663 10.667 16.6663H9.33366C8.96547 16.6663 8.66699 16.3679 8.66699 15.9997C8.66699 15.6315 8.96547 15.333 9.33366 15.333H10.667C11.0352 15.333 11.3337 15.6315 11.3337 15.9997Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_21157_107468">
<rect width="16" height="16" fill="white" transform="translate(8 8)" />
</clipPath>
</defs>
</svg>
);
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const OverviewIcon: React.FC<ISvgIcons> = ({ width = "16", height = "16", className = "" }) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.5 3C2.5 2.86739 2.55268 2.74021 2.64645 2.64645C2.74021 2.55268 2.86739 2.5 3 2.5H3.5C9.02267 2.5 13.5 6.97733 13.5 12.5V13C13.5 13.1326 13.4473 13.2598 13.3536 13.3536C13.2598 13.4473 13.1326 13.5 13 13.5H12.5C12.3674 13.5 12.2402 13.4473 12.1464 13.3536C12.0527 13.2598 12 13.1326 12 13V12.5C12 7.80533 8.19467 4 3.5 4H3C2.86739 4 2.74021 3.94732 2.64645 3.85355C2.55268 3.75979 2.5 3.63261 2.5 3.5V3ZM2.5 7.5C2.5 7.36739 2.55268 7.24022 2.64645 7.14645C2.74021 7.05268 2.86739 7 3 7H3.5C4.22227 7 4.93747 7.14226 5.60476 7.41866C6.27205 7.69506 6.87837 8.10019 7.38909 8.61091C7.89981 9.12164 8.30494 9.72795 8.58134 10.3952C8.85774 11.0625 9 11.7777 9 12.5V13C9 13.1326 8.94732 13.2598 8.85355 13.3536C8.75978 13.4473 8.63261 13.5 8.5 13.5H8C7.86739 13.5 7.74022 13.4473 7.64645 13.3536C7.55268 13.2598 7.5 13.1326 7.5 13V12.5C7.5 11.4391 7.07857 10.4217 6.32843 9.67157C5.57828 8.92143 4.56087 8.5 3.5 8.5H3C2.86739 8.5 2.74021 8.44732 2.64645 8.35355C2.55268 8.25978 2.5 8.13261 2.5 8V7.5ZM2.5 12.5C2.5 12.2348 2.60536 11.9804 2.79289 11.7929C2.98043 11.6054 3.23478 11.5 3.5 11.5C3.76522 11.5 4.01957 11.6054 4.20711 11.7929C4.39464 11.9804 4.5 12.2348 4.5 12.5C4.5 12.7652 4.39464 13.0196 4.20711 13.2071C4.01957 13.3946 3.76522 13.5 3.5 13.5C3.23478 13.5 2.98043 13.3946 2.79289 13.2071C2.60536 13.0196 2.5 12.7652 2.5 12.5Z"
fill="#455068"
/>
</svg>
);
+19
View File
@@ -0,0 +1,19 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const TeamsIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.25 6.75C8.25 5.75544 8.64509 4.80161 9.34835 4.09835C10.0516 3.39509 11.0054 3 12 3C12.9946 3 13.9484 3.39509 14.6517 4.09835C15.3549 4.80161 15.75 5.75544 15.75 6.75C15.75 7.74456 15.3549 8.69839 14.6517 9.40165C13.9484 10.1049 12.9946 10.5 12 10.5C11.0054 10.5 10.0516 10.1049 9.34835 9.40165C8.64509 8.69839 8.25 7.74456 8.25 6.75ZM15.75 9.75C15.75 8.95435 16.0661 8.19129 16.6287 7.62868C17.1913 7.06607 17.9544 6.75 18.75 6.75C19.5456 6.75 20.3087 7.06607 20.8713 7.62868C21.4339 8.19129 21.75 8.95435 21.75 9.75C21.75 10.5456 21.4339 11.3087 20.8713 11.8713C20.3087 12.4339 19.5456 12.75 18.75 12.75C17.9544 12.75 17.1913 12.4339 16.6287 11.8713C16.0661 11.3087 15.75 10.5456 15.75 9.75ZM2.25 9.75C2.25 8.95435 2.56607 8.19129 3.12868 7.62868C3.69129 7.06607 4.45435 6.75 5.25 6.75C6.04565 6.75 6.80871 7.06607 7.37132 7.62868C7.93393 8.19129 8.25 8.95435 8.25 9.75C8.25 10.5456 7.93393 11.3087 7.37132 11.8713C6.80871 12.4339 6.04565 12.75 5.25 12.75C4.45435 12.75 3.69129 12.4339 3.12868 11.8713C2.56607 11.3087 2.25 10.5456 2.25 9.75ZM6.31 15.117C6.91995 14.161 7.76108 13.3743 8.75562 12.8294C9.75016 12.2846 10.866 11.9994 12 12C12.9498 11.9991 13.8891 12.1989 14.7564 12.5862C15.6237 12.9734 16.3994 13.5395 17.0327 14.2474C17.6661 14.9552 18.1428 15.7888 18.4317 16.6936C18.7205 17.5985 18.815 18.5541 18.709 19.498C18.696 19.6153 18.6556 19.7278 18.591 19.8265C18.5263 19.9252 18.4393 20.0073 18.337 20.066C16.4086 21.1725 14.2233 21.7532 12 21.75C9.695 21.75 7.53 21.138 5.663 20.066C5.56069 20.0073 5.47368 19.9252 5.40904 19.8265C5.34441 19.7278 5.30396 19.6153 5.291 19.498C5.12305 17.9646 5.48246 16.4198 6.31 15.118V15.117Z"
fill="currentColor"
{...rest}
/>
<path
d="M5.08208 14.2539C4.09584 15.7763 3.63633 17.5802 3.77408 19.3889C3.17359 19.2979 2.58299 19.1505 2.01008 18.9489L1.89508 18.9089C1.79248 18.8725 1.70263 18.8071 1.63643 18.7207C1.57023 18.6342 1.53051 18.5305 1.52208 18.4219L1.51208 18.3009C1.47169 17.7989 1.53284 17.2938 1.69188 16.816C1.85093 16.3381 2.10462 15.8971 2.4378 15.5194C2.77099 15.1417 3.17685 14.835 3.63116 14.6176C4.08547 14.4001 4.57893 14.2765 5.08208 14.2539ZM20.2261 19.3889C20.3638 17.5802 19.9043 15.7763 18.9181 14.2539C19.4212 14.2765 19.9147 14.4001 20.369 14.6176C20.8233 14.835 21.2292 15.1417 21.5624 15.5194C21.8955 15.8971 22.1492 16.3381 22.3083 16.816C22.4673 17.2938 22.5285 17.7989 22.4881 18.3009L22.4781 18.4219C22.4695 18.5303 22.4297 18.6338 22.3635 18.7201C22.2973 18.8063 22.2075 18.8716 22.1051 18.9079L21.9901 18.9479C21.4231 19.1479 20.8341 19.2969 20.2261 19.3889Z"
fill="currentColor"
/>
</svg>
);
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
import { ISvgIcons } from "./type";
export const UpdatesIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.54325 5.056C8.46325 2.58867 11.4633 1 14.8333 1C14.9659 1 15.093 1.05268 15.1868 1.14645C15.2806 1.24021 15.3333 1.36739 15.3333 1.5C15.3333 4.87067 13.7446 7.87 11.2773 9.79067C11.3799 10.4335 11.3418 11.0909 11.1656 11.7176C10.9895 12.3443 10.6795 12.9253 10.257 13.4205C9.83454 13.9158 9.30964 14.3135 8.71854 14.5862C8.12744 14.8588 7.48422 15 6.83325 15C6.70064 15 6.57347 14.9473 6.4797 14.8536C6.38593 14.7598 6.33325 14.6326 6.33325 14.5V11.746C5.6852 11.2342 5.09942 10.6482 4.58792 10H1.83325C1.70064 10 1.57347 9.94732 1.4797 9.85355C1.38593 9.75979 1.33325 9.63261 1.33325 9.5C1.3332 8.84897 1.47441 8.20568 1.74713 7.61453C2.01986 7.02337 2.41761 6.49843 2.91293 6.07594C3.40824 5.65345 3.98934 5.34346 4.6161 5.16737C5.24287 4.99128 5.90038 4.95328 6.54325 5.056ZM10.3333 4.5C9.93543 4.5 9.5539 4.65804 9.27259 4.93934C8.99129 5.22064 8.83325 5.60218 8.83325 6C8.83325 6.39782 8.99129 6.77936 9.27259 7.06066C9.5539 7.34196 9.93543 7.5 10.3333 7.5C10.7311 7.5 11.1126 7.34196 11.3939 7.06066C11.6752 6.77936 11.8333 6.39782 11.8333 6C11.8333 5.60218 11.6752 5.22064 11.3939 4.93934C11.1126 4.65804 10.7311 4.5 10.3333 4.5Z"
fill="#8591AD"
/>
<path
d="M3.83994 11.4947C3.8926 11.4554 3.93701 11.4062 3.97064 11.3497C4.00426 11.2933 4.02645 11.2308 4.03592 11.1658C4.04539 11.1008 4.04197 11.0346 4.02584 10.9709C4.00972 10.9072 3.98121 10.8473 3.94194 10.7947C3.90268 10.742 3.85342 10.6976 3.797 10.664C3.74057 10.6304 3.67807 10.6082 3.61307 10.5987C3.54807 10.5892 3.48184 10.5927 3.41816 10.6088C3.35448 10.6249 3.2946 10.6534 3.24194 10.6927C2.73054 11.0731 2.33288 11.5861 2.092 12.1762C1.85111 12.7663 1.77617 13.4111 1.87528 14.0407C1.89139 14.1455 1.94045 14.2426 2.01536 14.3177C2.09026 14.3929 2.18713 14.4422 2.29194 14.4587C2.92163 14.5577 3.56641 14.4827 4.15652 14.2417C4.74664 14.0007 5.25961 13.6029 5.63994 13.0913C5.68047 13.0388 5.71015 12.9788 5.72723 12.9147C5.74432 12.8506 5.74848 12.7837 5.73948 12.718C5.73047 12.6522 5.70847 12.589 5.67476 12.5318C5.64105 12.4747 5.59631 12.4248 5.54315 12.3852C5.48998 12.3455 5.42945 12.3168 5.36508 12.3007C5.30071 12.2847 5.23379 12.2816 5.16821 12.2917C5.10264 12.3017 5.03973 12.3248 4.98314 12.3594C4.92655 12.394 4.87742 12.4395 4.83861 12.4933C4.60612 12.806 4.30366 13.0599 3.95544 13.2347C3.60722 13.4095 3.22291 13.5004 2.83328 13.5C2.83328 12.68 3.22794 11.9513 3.83994 11.4947Z"
fill="#8591AD"
/>
</svg>
);
+27 -5
View File
@@ -1,4 +1,4 @@
import React, { FC, Fragment } from "react";
import React, { FC, Fragment, useEffect, useState } from "react";
import { Tab } from "@headlessui/react";
import { LucideProps } from "lucide-react";
// helpers
@@ -11,11 +11,12 @@ type TabItem = {
label?: React.ReactNode;
content: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
type TTabsProps = {
tabs: TabItem[];
storageKey: string;
storageKey?: string;
actions?: React.ReactNode;
defaultTab?: string;
containerClassName?: string;
@@ -23,6 +24,8 @@ type TTabsProps = {
tabListClassName?: string;
tabClassName?: string;
tabPanelClassName?: string;
size?: "sm" | "md" | "lg";
storeInLocalStorage?: boolean;
};
export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
@@ -36,15 +39,28 @@ export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
tabListClassName = "",
tabClassName = "",
tabPanelClassName = "",
size = "md",
storeInLocalStorage = true,
} = props;
// local storage
const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab);
const { storedValue, setValue } = useLocalStorage(
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
defaultTab
);
// state
const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
useEffect(() => {
if (storeInLocalStorage) {
setValue(selectedTab);
}
}, [selectedTab, setValue, storeInLocalStorage, storageKey]);
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
return (
<div className="flex flex-col w-full h-full">
<Tab.Group defaultIndex={currentTabIndex(storedValue ?? defaultTab)}>
<Tab.Group defaultIndex={currentTabIndex(selectedTab)}>
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
<Tab.List
@@ -64,12 +80,18 @@ export const Tabs: FC<TTabsProps> = (props: TTabsProps) => {
: tab.disabled
? "text-custom-text-400 cursor-not-allowed"
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
{
"text-xs": size === "sm",
"text-sm": size === "md",
"text-base": size === "lg",
},
tabClassName
)
}
key={tab.key}
onClick={() => {
if (!tab.disabled) setValue(tab.key);
if (!tab.disabled) setSelectedTab(tab.key);
tab.onClick?.();
}}
disabled={tab.disabled}
>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "0.24.0",
"version": "0.24.1",
"description": "Helper functions shared across multiple apps internally",
"private": true,
"main": "./dist/index.js",
@@ -26,7 +26,7 @@ import { CoreRootStore } from "../root.store";
// constants
// helpers
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null> | "target_date";
export type TIssueDisplayFilterOptions = Exclude<TIssueGroupByOptions, null | "team_project"> | "target_date";
export enum EIssueGroupedAction {
ADD = "ADD",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.24.0",
"version": "0.24.1",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -1,7 +1,7 @@
"use client";
// components
import { NotificationsSidebarRoot } from "@/plane-web/components/workspace-notifications";
import { NotificationsSidebarRoot } from "@/components/workspace-notifications";
export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
return (
@@ -77,7 +77,12 @@ const CycleDetailPage = observer(() => {
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<CycleDetailsSidebar handleClose={toggleSidebar} />
<CycleDetailsSidebar
handleClose={toggleSidebar}
cycleId={cycleId.toString()}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
</div>
)}
</div>
@@ -1,130 +1,3 @@
"use client";
import { IssuesHeader } from "@/plane-web/components/issues";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Briefcase, Circle, ExternalLink } from "lucide-react";
// ui
import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui";
// components
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
// constants
import HeaderFilters from "@/components/issues/filters";
import { EIssuesStoreType } from "@/constants/issue";
// helpers
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
// hooks
import { useEventTracker, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const ProjectIssuesHeader = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT);
const { currentProjectDetails, loader } = useProject();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS();
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
const canUserCreateIssue = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return (
<Header>
<Header.LeftItem>
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</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>
{issuesCount && issuesCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
>
<CountChip count={issuesCount} />
</Tooltip>
) : null}
</div>
{currentProjectDetails?.anchor ? (
<a
href={publishedURL}
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>
) : (
<></>
)}
</Header.LeftItem>
<Header.RightItem>
<div className="hidden gap-3 md:flex">
<HeaderFilters
projectId={projectId}
currentProjectDetails={currentProjectDetails}
workspaceSlug={workspaceSlug}
canUserCreateIssue={canUserCreateIssue}
/>
</div>
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("Project issues page");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
size="sm"
>
<div className="hidden sm:block">Add</div> Issue
</Button>
) : (
<></>
)}
</Header.RightItem>
</Header>
);
});
export const ProjectIssuesHeader = () => <IssuesHeader />;
+13 -4
View File
@@ -21,6 +21,7 @@ import { useFavorite } from "@/hooks/store/use-favorite";
import useSize from "@/hooks/use-window-size";
// plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const AppSidebar: FC = observer(() => {
@@ -47,7 +48,7 @@ export const AppSidebar: FC = observer(() => {
});
useEffect(() => {
if (windowSize[0] < 768) !sidebarCollapsed && toggleSidebar();
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize]);
@@ -73,9 +74,12 @@ export const AppSidebar: FC = observer(() => {
"px-4": !sidebarCollapsed,
})}
>
{/* Workspace switcher and settings */}
<SidebarDropdown />
<div className="flex-shrink-0 h-4" />
<SidebarAppSwitcher />
{/* App switcher */}
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
{/* Quick actions */}
<SidebarQuickActions />
</div>
<hr
@@ -88,18 +92,23 @@ export const AppSidebar: FC = observer(() => {
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
{/* User Menu */}
<SidebarUserMenu />
{/* Workspace Menu */}
<SidebarWorkspaceMenu />
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
"opacity-0": !sidebarCollapsed,
})}
/>
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<SidebarHelpSection />
</div>
</div>
@@ -0,0 +1,3 @@
export * from "./workspace-level";
export * from "./project-level";
export * from "./issue-level";
@@ -0,0 +1,73 @@
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import useSWR from "swr";
// components
import { BulkDeleteIssuesModal } from "@/components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
// constants
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
// hooks
import { useCommandPalette, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
// services
import { IssueService } from "@/services/issue";
// services
const issueService = new IssueService();
export const IssueLevelModals = observer(() => {
// router
const pathname = usePathname();
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams();
const router = useAppRouter();
// store hooks
const { data: currentUser } = useUser();
const {
issues: { removeIssue },
} = useIssuesStore();
const {
isCreateIssueModalOpen,
toggleCreateIssueModal,
isDeleteIssueModalOpen,
toggleDeleteIssueModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
} = useCommandPalette();
// derived values
const isDraftIssue = pathname?.includes("draft-issues") || false;
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
return (
<>
<CreateUpdateIssueModal
isOpen={isCreateIssueModalOpen}
onClose={() => toggleCreateIssueModal(false)}
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
isDraft={isDraftIssue}
/>
{workspaceSlug && projectId && issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => toggleDeleteIssueModal(false)}
isOpen={isDeleteIssueModalOpen}
data={issueDetails}
onSubmit={async () => {
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
}}
/>
)}
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssueModalOpen}
onClose={() => toggleBulkDeleteIssueModal(false)}
user={currentUser}
/>
</>
);
});
@@ -0,0 +1,59 @@
import { observer } from "mobx-react";
// components
import { CycleCreateUpdateModal } from "@/components/cycles";
import { CreateUpdateModuleModal } from "@/components/modules";
import { CreatePageModal } from "@/components/pages";
import { CreateUpdateProjectViewModal } from "@/components/views";
// hooks
import { useCommandPalette } from "@/hooks/store";
export type TProjectLevelModalsProps = {
workspaceSlug: string;
projectId: string;
};
export const ProjectLevelModals = observer((props: TProjectLevelModalsProps) => {
const { workspaceSlug, projectId } = props;
// store hooks
const {
isCreateCycleModalOpen,
toggleCreateCycleModal,
isCreateModuleModalOpen,
toggleCreateModuleModal,
isCreateViewModalOpen,
toggleCreateViewModal,
createPageModal,
toggleCreatePageModal,
} = useCommandPalette();
return (
<>
<CycleCreateUpdateModal
isOpen={isCreateCycleModalOpen}
handleClose={() => toggleCreateCycleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
onClose={() => toggleCreateModuleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateProjectViewModal
isOpen={isCreateViewModalOpen}
onClose={() => toggleCreateViewModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreatePageModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isModalOpen={createPageModal.isOpen}
pageAccess={createPageModal.pageAccess}
handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
redirectionEnabled
/>
</>
);
});
@@ -0,0 +1,25 @@
import { observer } from "mobx-react";
// components
import { CreateProjectModal } from "@/components/project";
// hooks
import { useCommandPalette } from "@/hooks/store";
export type TWorkspaceLevelModalsProps = {
workspaceSlug: string;
};
export const WorkspaceLevelModals = observer((props: TWorkspaceLevelModalsProps) => {
const { workspaceSlug } = props;
// store hooks
const { isCreateProjectModalOpen, toggleCreateProjectModal } = useCommandPalette();
return (
<>
<CreateProjectModal
isOpen={isCreateProjectModalOpen}
onClose={() => toggleCreateProjectModal(false)}
workspaceSlug={workspaceSlug.toString()}
/>
</>
);
});
+65 -52
View File
@@ -1,5 +1,6 @@
"use client";
import { useMemo } from "react";
import { observer } from "mobx-react";
import { Disclosure } from "@headlessui/react";
// ui
@@ -22,68 +23,80 @@ import { ActiveCycleIssueDetails } from "@/store/issue/cycle";
interface IActiveCycleDetails {
workspaceSlug: string;
projectId: string;
cycleId?: string;
showHeader?: boolean;
}
export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) => {
const { workspaceSlug, projectId } = props;
const { currentProjectActiveCycle, currentProjectActiveCycleId } = useCycle();
const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props;
const { currentProjectActiveCycleId } = useCycle();
// derived values
const cycleId = propsCycleId ?? currentProjectActiveCycleId;
// fetch cycle details
const {
handleFiltersUpdate,
cycle: activeCycle,
cycleIssueDetails,
} = useCyclesDetails({ workspaceSlug, projectId, cycleId: currentProjectActiveCycleId });
} = useCyclesDetails({ workspaceSlug, projectId, cycleId });
const ActiveCyclesComponent = useMemo(
() => (
<>
{!cycleId || !activeCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{cycleId && (
<CyclesListItem
key={cycleId}
cycleId={cycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={cycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</>
),
[cycleId, activeCycle, workspaceSlug, projectId, handleFiltersUpdate, cycleIssueDetails]
);
return (
<>
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>
{!currentProjectActiveCycle ? (
<EmptyState type={EmptyStateType.PROJECT_CYCLE_ACTIVE} size="sm" />
) : (
<div className="flex flex-col border-b border-custom-border-200">
{currentProjectActiveCycleId && (
<CyclesListItem
key={currentProjectActiveCycleId}
cycleId={currentProjectActiveCycleId}
workspaceSlug={workspaceSlug}
projectId={projectId}
className="!border-b-transparent"
/>
)}
<Row className="bg-custom-background-100 pt-3 pb-6">
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
<ActiveCycleProgress
handleFiltersUpdate={handleFiltersUpdate}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycle={activeCycle}
/>
<ActiveCycleProductivity
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
/>
<ActiveCycleStats
workspaceSlug={workspaceSlug}
projectId={projectId}
cycle={activeCycle}
cycleId={currentProjectActiveCycleId}
handleFiltersUpdate={handleFiltersUpdate}
cycleIssueDetails={cycleIssueDetails as ActiveCycleIssueDetails}
/>
</div>
</Row>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
{showHeader ? (
<Disclosure as="div" className="flex flex-shrink-0 flex-col" defaultOpen>
{({ open }) => (
<>
<Disclosure.Button className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-custom-border-200 bg-custom-background-90 cursor-pointer">
<CycleListGroupHeader title="Active cycle" type="current" isExpanded={open} />
</Disclosure.Button>
<Disclosure.Panel>{ActiveCyclesComponent}</Disclosure.Panel>
</>
)}
</Disclosure>
) : (
<>{ActiveCyclesComponent}</>
)}
</>
);
});
@@ -1,2 +1,3 @@
export * from "./applied-filters";
export * from "./issue-types";
export * from "./team-project";
@@ -0,0 +1,12 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
type Props = {
appliedFilters: string[] | null;
handleUpdate: (val: string) => void;
searchQuery: string;
};
export const FilterTeamProjects: React.FC<Props> = observer(() => null);
+130
View File
@@ -0,0 +1,130 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// icons
import { Briefcase, Circle, ExternalLink } from "lucide-react";
// ui
import { Breadcrumbs, Button, LayersIcon, Tooltip, Header } from "@plane/ui";
// components
import { BreadcrumbLink, CountChip, Logo } from "@/components/common";
// constants
import HeaderFilters from "@/components/issues/filters";
import { EIssuesStoreType } from "@/constants/issue";
// helpers
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
// hooks
import { useEventTracker, useProject, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useIssues } from "@/hooks/store/use-issues";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export const IssuesHeader = observer(() => {
// router
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
issues: { getGroupIssueCount },
} = useIssues(EIssuesStoreType.PROJECT);
const { currentProjectDetails, loader } = useProject();
const { toggleCreateIssueModal } = useCommandPalette();
const { setTrackElement } = useEventTracker();
const { allowPermissions } = useUserPermissions();
const { isMobile } = usePlatformOS();
const SPACE_APP_URL = (SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH;
const publishedURL = `${SPACE_APP_URL}/issues/${currentProjectDetails?.anchor}`;
const issuesCount = getGroupIssueCount(undefined, undefined, false);
const canUserCreateIssue = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
return (
<Header>
<Header.LeftItem>
<div className="flex items-center gap-2.5">
<Breadcrumbs onBack={() => router.back()} isLoading={loader}>
<Breadcrumbs.BreadcrumbItem
type="text"
link={
<BreadcrumbLink
label={currentProjectDetails?.name ?? "Project"}
icon={
currentProjectDetails ? (
currentProjectDetails && (
<span className="grid place-items-center flex-shrink-0 h-4 w-4">
<Logo logo={currentProjectDetails?.logo_props} size={16} />
</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>
{issuesCount && issuesCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issuesCount} ${issuesCount > 1 ? "issues" : "issue"} in this project`}
position="bottom"
>
<CountChip count={issuesCount} />
</Tooltip>
) : null}
</div>
{currentProjectDetails?.anchor ? (
<a
href={publishedURL}
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>
) : (
<></>
)}
</Header.LeftItem>
<Header.RightItem>
<div className="hidden gap-3 md:flex">
<HeaderFilters
projectId={projectId}
currentProjectDetails={currentProjectDetails}
workspaceSlug={workspaceSlug}
canUserCreateIssue={canUserCreateIssue}
/>
</div>
{canUserCreateIssue ? (
<Button
onClick={() => {
setTrackElement("Project issues page");
toggleCreateIssueModal(true, EIssuesStoreType.PROJECT);
}}
size="sm"
>
<div className="hidden sm:block">Add</div> Issue
</Button>
) : (
<></>
)}
</Header.RightItem>
</Header>
);
});
+1
View File
@@ -4,3 +4,4 @@ export * from "./issue-modal";
export * from "./issue-details";
export * from "./quick-add";
export * from "./filters";
export * from "./header";
@@ -0,0 +1,4 @@
// types
import { IGroupByColumn } from "@plane/types";
export const getTeamProjectColumns = (): IGroupByColumn[] | undefined => undefined;
@@ -1 +1 @@
export * from './root'
export * from "./notification-card/root";
@@ -0,0 +1 @@
export const SidebarTeamsList = () => null;
+53 -5
View File
@@ -1,17 +1,19 @@
"use client";
// icons
import { Home, Inbox, PenSquare } from "lucide-react";
import { Briefcase, Home, Inbox, Layers, PenSquare, BarChart2 } from "lucide-react";
// ui
import { UserActivityIcon } from "@plane/ui";
import { UserActivityIcon, ContrastIcon } from "@plane/ui";
import { Props } from "@/components/icons/types";
// constants
import { TLinkOptions } from "@/constants/dashboard";
// plane web constants
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
// plane web types
import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
export type TSidebarUserMenuItems = {
key: TSidebarUserMenuItemKeys;
export type TSidebarMenuItems<T extends TSidebarUserMenuItemKeys | TSidebarWorkspaceMenuItemKeys> = {
key: T;
label: string;
href: string;
access: EUserPermissions[];
@@ -19,6 +21,8 @@ export type TSidebarUserMenuItems = {
Icon: React.FC<Props>;
};
export type TSidebarUserMenuItems = TSidebarMenuItems<TSidebarUserMenuItemKeys>;
export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
{
key: "home",
@@ -54,3 +58,47 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [
Icon: PenSquare,
},
];
export type TSidebarWorkspaceMenuItems = TSidebarMenuItems<TSidebarWorkspaceMenuItemKeys>;
export const SIDEBAR_WORKSPACE_MENU: Partial<Record<TSidebarWorkspaceMenuItemKeys, TSidebarWorkspaceMenuItems>> = {
projects: {
key: "projects",
label: "Projects",
href: `/projects`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/projects/`,
Icon: Briefcase,
},
"all-issues": {
key: "all-issues",
label: "Views",
href: `/workspace-views/all-issues`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/workspace-views/`),
Icon: Layers,
},
"active-cycles": {
key: "active-cycles",
label: "Cycles",
href: `/active-cycles`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/active-cycles/`,
Icon: ContrastIcon,
},
analytics: {
key: "analytics",
label: "Analytics",
href: `/analytics`,
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/analytics/`),
Icon: BarChart2,
},
};
export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [
SIDEBAR_WORKSPACE_MENU?.projects,
SIDEBAR_WORKSPACE_MENU?.["all-issues"],
SIDEBAR_WORKSPACE_MENU?.["active-cycles"],
SIDEBAR_WORKSPACE_MENU?.analytics,
].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined);
+7 -1
View File
@@ -1,4 +1,6 @@
import { TIssueActivityComment } from "@plane/types";
// constants
import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
@@ -19,7 +21,7 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<EActivityFilterType, { label:
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
export type TActivityFilterOption = {
key: EActivityFilterType;
key: TActivityFilters;
label: string;
isSelected: boolean;
onClick: () => void;
@@ -32,3 +34,7 @@ export const filterActivityOnSelectedFilters = (
activity.filter((activity) => filter.includes(activity.activity_type as TActivityFilters));
export const ENABLE_ISSUE_DEPENDENCIES = false;
export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions };
} = {};
+95
View File
@@ -0,0 +1,95 @@
// types
import { TCommandPaletteActionList, TCommandPaletteShortcut, TCommandPaletteShortcutList } from "@plane/types";
// store
import { store } from "@/lib/store-context";
export const getGlobalShortcutsList: () => TCommandPaletteActionList = () => {
const { toggleCreateIssueModal } = store.commandPalette;
return {
c: {
title: "Create a new issue",
description: "Create a new issue in the current project",
action: () => toggleCreateIssueModal(true),
},
};
};
export const getWorkspaceShortcutsList: () => TCommandPaletteActionList = () => {
const { toggleCreateProjectModal } = store.commandPalette;
return {
p: {
title: "Create a new project",
description: "Create a new project in the current workspace",
action: () => toggleCreateProjectModal(true),
},
};
};
export const getProjectShortcutsList: () => TCommandPaletteActionList = () => {
const {
toggleCreatePageModal,
toggleCreateModuleModal,
toggleCreateCycleModal,
toggleCreateViewModal,
toggleBulkDeleteIssueModal,
} = store.commandPalette;
return {
d: {
title: "Create a new page",
description: "Create a new page in the current project",
action: () => toggleCreatePageModal({ isOpen: true }),
},
m: {
title: "Create a new module",
description: "Create a new module in the current project",
action: () => toggleCreateModuleModal(true),
},
q: {
title: "Create a new cycle",
description: "Create a new cycle in the current project",
action: () => toggleCreateCycleModal(true),
},
v: {
title: "Create a new view",
description: "Create a new view in the current project",
action: () => toggleCreateViewModal(true),
},
backspace: {
title: "Bulk delete issues",
description: "Bulk delete issues in the current project",
action: () => toggleBulkDeleteIssueModal(true),
},
delete: {
title: "Bulk delete issues",
description: "Bulk delete issues in the current project",
action: () => toggleBulkDeleteIssueModal(true),
},
};
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const handleAdditionalKeyDownEvents = (e: KeyboardEvent) => null;
export const getNavigationShortcutsList = (): TCommandPaletteShortcut[] => [
{ keys: "Ctrl,K", description: "Open command menu" },
];
export const getCommonShortcutsList = (platform: string): TCommandPaletteShortcut[] => [
{ keys: "P", description: "Create project" },
{ keys: "C", description: "Create issue" },
{ keys: "Q", description: "Create cycle" },
{ keys: "M", description: "Create module" },
{ keys: "V", description: "Create view" },
{ keys: "D", description: "Create page" },
{ keys: "Delete", description: "Bulk delete issues" },
{ keys: "Shift,/", description: "Open shortcuts guide" },
{
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
description: "Copy issue URL from the issue details page",
},
];
export const getAdditionalShortcutsList = (): TCommandPaletteShortcutList[] => [];
+4 -1
View File
@@ -1,5 +1,8 @@
// plane web types
import { TSidebarUserMenuItemKeys } from "@/plane-web/types/dashboard";
import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isUserFeatureEnabled = (featureKey: TSidebarUserMenuItemKeys) => true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const isWorkspaceFeatureEnabled = (featureKey: TSidebarWorkspaceMenuItemKeys, workspaceSlug: string) => true;
+15
View File
@@ -0,0 +1,15 @@
import { IssueActions } from "@/hooks/use-issues-actions";
export const useTeamIssueActions: () => IssueActions = () => ({
fetchIssues: () => Promise.resolve(undefined),
fetchNextIssues: () => Promise.resolve(undefined),
removeIssue: () => Promise.resolve(undefined),
updateFilters: () => Promise.resolve(undefined),
});
export const useTeamViewIssueActions: () => IssueActions = () => ({
fetchIssues: () => Promise.resolve(undefined),
fetchNextIssues: () => Promise.resolve(undefined),
removeIssue: () => Promise.resolve(undefined),
updateFilters: () => Promise.resolve(undefined),
});
+26
View File
@@ -0,0 +1,26 @@
import { computed, makeObservable } from "mobx";
// types / constants
import { BaseCommandPaletteStore, IBaseCommandPaletteStore } from "@/store/base-command-palette.store";
export interface ICommandPaletteStore extends IBaseCommandPaletteStore {
// computed
isAnyModalOpen: boolean;
}
export class CommandPaletteStore extends BaseCommandPaletteStore implements ICommandPaletteStore {
constructor() {
super();
makeObservable(this, {
// computed
isAnyModalOpen: computed,
});
}
/**
* Checks whether any modal is open or not in the base command palette.
* @returns boolean
*/
get isAnyModalOpen(): boolean {
return Boolean(super.getCoreModalsState());
}
}
@@ -0,0 +1,12 @@
import { IProjectViewIssuesFilter, ProjectViewIssuesFilter } from "@/store/issue/project-views";
import { IIssueRootStore } from "@/store/issue/root.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type ITeamViewIssuesFilter = IProjectViewIssuesFilter;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class TeamViewIssuesFilter extends ProjectViewIssuesFilter implements IProjectViewIssuesFilter {
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";
@@ -0,0 +1,13 @@
import { IProjectViewIssues, ProjectViewIssues } from "@/store/issue/project-views";
import { IIssueRootStore } from "@/store/issue/root.store";
import { ITeamViewIssuesFilter } from "./filter.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type ITeamViewIssues = IProjectViewIssues;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class TeamViewIssues extends ProjectViewIssues implements IProjectViewIssues {
constructor(_rootStore: IIssueRootStore, teamViewFilterStore: ITeamViewIssuesFilter) {
super(_rootStore, teamViewFilterStore);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { IProjectIssuesFilter, ProjectIssuesFilter } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type ITeamIssuesFilter = IProjectIssuesFilter;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class TeamIssuesFilter extends ProjectIssuesFilter implements IProjectIssuesFilter {
constructor(_rootStore: IIssueRootStore) {
super(_rootStore);
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./filter.store";
export * from "./issue.store";
+13
View File
@@ -0,0 +1,13 @@
import { IProjectIssues, ProjectIssues } from "@/store/issue/project";
import { IIssueRootStore } from "@/store/issue/root.store";
import { ITeamIssuesFilter } from "./filter.store";
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export type ITeamIssues = IProjectIssues;
// @ts-nocheck - This class will never be used, extending similar class to avoid type errors
export class TeamIssues extends ProjectIssues implements IProjectIssues {
constructor(_rootStore: IIssueRootStore, teamIssueFilterStore: ITeamIssuesFilter) {
super(_rootStore, teamIssueFilterStore);
}
}
+2
View File
@@ -1 +1,3 @@
export type TSidebarUserMenuItemKeys = "home" | "your-work" | "notifications" | "drafts";
export type TSidebarWorkspaceMenuItemKeys = "projects" | "all-issues" | "active-cycles" | "analytics";
@@ -2,87 +2,43 @@
import React, { useCallback, useEffect, FC, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import useSWR from "swr";
import { useParams } from "next/navigation";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { CommandModal, ShortcutsModal } from "@/components/command-palette";
import { BulkDeleteIssuesModal } from "@/components/core";
import { CycleCreateUpdateModal } from "@/components/cycles";
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
import { CreateUpdateModuleModal } from "@/components/modules";
import { CreatePageModal } from "@/components/pages";
import { CreateProjectModal } from "@/components/project";
import { CreateUpdateProjectViewModal } from "@/components/views";
// constants
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
// helpers
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useUser, useAppTheme, useCommandPalette, useUserPermissions } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { useIssuesStore } from "@/hooks/use-issue-layout-store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import {
IssueLevelModals,
ProjectLevelModals,
WorkspaceLevelModals,
} from "@/plane-web/components/command-palette/modals";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services
import { IssueService } from "@/services/issue";
// services
const issueService = new IssueService();
// plane web helpers
import {
getGlobalShortcutsList,
getProjectShortcutsList,
getWorkspaceShortcutsList,
handleAdditionalKeyDownEvents,
} from "@/plane-web/helpers/command-palette";
export const CommandPalette: FC = observer(() => {
// router
const router = useAppRouter();
// router params
const { workspaceSlug, projectId, issueId, cycleId, moduleId } = useParams();
// pathname
const pathname = usePathname();
const { workspaceSlug, projectId, issueId } = useParams();
// store hooks
const { toggleSidebar } = useAppTheme();
const { setTrackElement } = useEventTracker();
const { platform } = usePlatformOS();
const {
data: currentUser,
// canPerformProjectMemberActions,
// canPerformWorkspaceMemberActions,
canPerformAnyCreateAction,
// canPerformProjectAdminActions,
} = useUser();
const {
issues: { removeIssue },
} = useIssuesStore();
const {
toggleCommandPaletteModal,
isCreateIssueModalOpen,
toggleCreateIssueModal,
isCreateCycleModalOpen,
toggleCreateCycleModal,
createPageModal,
toggleCreatePageModal,
isCreateProjectModalOpen,
toggleCreateProjectModal,
isCreateModuleModalOpen,
toggleCreateModuleModal,
isCreateViewModalOpen,
toggleCreateViewModal,
isShortcutModalOpen,
toggleShortcutModal,
isBulkDeleteIssueModalOpen,
toggleBulkDeleteIssueModal,
isDeleteIssueModalOpen,
toggleDeleteIssueModal,
isAnyModalOpen,
} = useCommandPalette();
const { data: currentUser, canPerformAnyCreateAction } = useUser();
const { toggleCommandPaletteModal, isShortcutModalOpen, toggleShortcutModal, isAnyModalOpen } = useCommandPalette();
const { allowPermissions } = useUserPermissions();
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
// derived values
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -170,62 +126,11 @@ export const CommandPalette: FC = observer(() => {
project: Record<string, { title: string; description: string; action: () => void }>;
} = useMemo(
() => ({
global: {
c: {
title: "Create a new issue",
description: "Create a new issue in the current project",
action: () => toggleCreateIssueModal(true),
},
},
workspace: {
p: {
title: "Create a new project",
description: "Create a new project in the current workspace",
action: () => toggleCreateProjectModal(true),
},
},
project: {
d: {
title: "Create a new page",
description: "Create a new page in the current project",
action: () => toggleCreatePageModal({ isOpen: true }),
},
m: {
title: "Create a new module",
description: "Create a new module in the current project",
action: () => toggleCreateModuleModal(true),
},
q: {
title: "Create a new cycle",
description: "Create a new cycle in the current project",
action: () => toggleCreateCycleModal(true),
},
v: {
title: "Create a new view",
description: "Create a new view in the current project",
action: () => toggleCreateViewModal(true),
},
backspace: {
title: "Bulk delete issues",
description: "Bulk delete issues in the current project",
action: () => toggleBulkDeleteIssueModal(true),
},
delete: {
title: "Bulk delete issues",
description: "Bulk delete issues in the current project",
action: () => toggleBulkDeleteIssueModal(true),
},
},
global: getGlobalShortcutsList(),
workspace: getWorkspaceShortcutsList(),
project: getProjectShortcutsList(),
}),
[
toggleBulkDeleteIssueModal,
toggleCreateCycleModal,
toggleCreateIssueModal,
toggleCreateModuleModal,
toggleCreatePageModal,
toggleCreateProjectModal,
toggleCreateViewModal,
]
[]
);
const handleKeyDown = useCallback(
@@ -296,6 +201,8 @@ export const CommandPalette: FC = observer(() => {
shortcutsList.project[keyPressed].action();
}
}
// Additional keydown events
handleAdditionalKeyDownEvents(e);
},
[
copyIssueUrlToClipboard,
@@ -320,75 +227,16 @@ export const CommandPalette: FC = observer(() => {
return () => document.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);
const isDraftIssue = pathname?.includes("draft-issues") || false;
if (!currentUser) return null;
return (
<>
<ShortcutsModal isOpen={isShortcutModalOpen} onClose={() => toggleShortcutModal(false)} />
{workspaceSlug && (
<CreateProjectModal
isOpen={isCreateProjectModalOpen}
onClose={() => toggleCreateProjectModal(false)}
workspaceSlug={workspaceSlug.toString()}
/>
)}
{workspaceSlug && <WorkspaceLevelModals workspaceSlug={workspaceSlug.toString()} />}
{workspaceSlug && projectId && (
<>
<CycleCreateUpdateModal
isOpen={isCreateCycleModalOpen}
handleClose={() => toggleCreateCycleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateModuleModal
isOpen={isCreateModuleModalOpen}
onClose={() => toggleCreateModuleModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreateUpdateProjectViewModal
isOpen={isCreateViewModalOpen}
onClose={() => toggleCreateViewModal(false)}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
/>
<CreatePageModal
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
isModalOpen={createPageModal.isOpen}
pageAccess={createPageModal.pageAccess}
handleModalClose={() => toggleCreatePageModal({ isOpen: false })}
redirectionEnabled
/>
</>
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
)}
<CreateUpdateIssueModal
isOpen={isCreateIssueModalOpen}
onClose={() => toggleCreateIssueModal(false)}
data={cycleId ? { cycle_id: cycleId.toString() } : moduleId ? { module_ids: [moduleId.toString()] } : undefined}
isDraft={isDraftIssue}
/>
{workspaceSlug && projectId && issueId && issueDetails && (
<DeleteIssueModal
handleClose={() => toggleDeleteIssueModal(false)}
isOpen={isDeleteIssueModalOpen}
data={issueDetails}
onSubmit={async () => {
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString());
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
}}
/>
)}
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssueModalOpen}
onClose={() => toggleBulkDeleteIssueModal(false)}
user={currentUser}
/>
<IssueLevelModals />
<CommandModal />
</>
);
@@ -3,6 +3,12 @@ import { Command } from "lucide-react";
import { substringMatch } from "@/helpers/string.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web helpers
import {
getAdditionalShortcutsList,
getCommonShortcutsList,
getNavigationShortcutsList,
} from "@/plane-web/helpers/command-palette";
type Props = {
searchQuery: string;
@@ -16,26 +22,14 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
{
key: "navigation",
title: "Navigation",
shortcuts: [{ keys: "Ctrl,K", description: "Open command menu" }],
shortcuts: getNavigationShortcutsList(),
},
{
key: "common",
title: "Common",
shortcuts: [
{ keys: "P", description: "Create project" },
{ keys: "C", description: "Create issue" },
{ keys: "Q", description: "Create cycle" },
{ keys: "M", description: "Create module" },
{ keys: "V", description: "Create view" },
{ keys: "D", description: "Create page" },
{ keys: "Delete", description: "Bulk delete issues" },
{ keys: "Shift,/", description: "Open shortcuts guide" },
{
keys: platform === "MacOS" ? "Ctrl,control,C" : "Ctrl,Alt,C",
description: "Copy issue URL from the issue details page",
},
],
shortcuts: getCommonShortcutsList(platform),
},
...getAdditionalShortcutsList(),
];
const filteredShortcuts = KEYBOARD_SHORTCUTS.map((category) => {
@@ -69,7 +63,11 @@ export const ShortcutCommandsList: React.FC<Props> = (props) => {
<div key={key} className="flex items-center gap-1">
{key === "Ctrl" ? (
<div className="grid h-6 min-w-[1.5rem] place-items-center rounded-sm border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 text-[10px] text-custom-text-200">
{ platform === "MacOS" ? <Command className="h-2.5 w-2.5 text-custom-text-200" /> : 'Ctrl'}
{platform === "MacOS" ? (
<Command className="h-2.5 w-2.5 text-custom-text-200" />
) : (
"Ctrl"
)}
</div>
) : (
<kbd className="grid h-6 min-w-[1.5rem] place-items-center rounded-sm border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 text-[10px] text-custom-text-200">
@@ -2,7 +2,6 @@
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { Loader } from "@plane/ui";
// components
@@ -13,19 +12,19 @@ import useCyclesDetails from "../active-cycle/use-cycles-details";
type Props = {
handleClose: () => void;
isArchived?: boolean;
cycleId?: string;
cycleId: string;
projectId: string;
workspaceSlug: string;
};
export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const { handleClose, isArchived } = props;
// router
const { workspaceSlug, projectId, cycleId } = useParams();
const { handleClose, isArchived, projectId, workspaceSlug, cycleId } = props;
// store hooks
const { cycle: cycleDetails } = useCyclesDetails({
workspaceSlug: workspaceSlug.toString(),
projectId: projectId.toString(),
cycleId: cycleId?.toString() || props.cycleId,
workspaceSlug,
projectId,
cycleId,
});
if (!cycleDetails)
@@ -47,21 +46,17 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="relative pb-2">
<div className="flex flex-col gap-5 w-full">
<CycleSidebarHeader
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug}
projectId={projectId}
cycleDetails={cycleDetails}
isArchived={isArchived}
handleClose={handleClose}
/>
<CycleSidebarDetails projectId={projectId.toString()} cycleDetails={cycleDetails} />
<CycleSidebarDetails projectId={projectId} cycleDetails={cycleDetails} />
</div>
{workspaceSlug && projectId && cycleDetails?.id && (
<CycleAnalyticsProgress
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
cycleId={cycleDetails?.id}
/>
<CycleAnalyticsProgress workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleDetails?.id} />
)}
</div>
);
@@ -9,12 +9,13 @@ import { useAppRouter } from "@/hooks/use-app-router";
import { CycleDetailsSidebar } from "./";
type Props = {
projectId: string;
projectId?: string;
workspaceSlug: string;
isArchived?: boolean;
};
export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspaceSlug, isArchived = false }) => {
export const CyclePeekOverview: React.FC<Props> = observer((props) => {
const { projectId: propsProjectId, workspaceSlug, isArchived } = props;
// router
const router = useAppRouter();
const pathname = usePathname();
@@ -23,22 +24,25 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
// refs
const ref = React.useRef(null);
// store hooks
const { fetchCycleDetails, fetchArchivedCycleDetails } = useCycle();
const { getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle();
// derived values
const cycleDetails = peekCycle ? getCycleById(peekCycle.toString()) : undefined;
const projectId = propsProjectId || cycleDetails?.project_id;
const handleClose = () => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
router.push(`${pathname}?${query}`);
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
};
useEffect(() => {
if (!peekCycle) return;
if (!peekCycle || !projectId) return;
if (isArchived) fetchArchivedCycleDetails(workspaceSlug, projectId, peekCycle.toString());
else fetchCycleDetails(workspaceSlug, projectId, peekCycle.toString());
}, [fetchArchivedCycleDetails, fetchCycleDetails, isArchived, peekCycle, projectId, workspaceSlug]);
return (
<>
{peekCycle && (
{peekCycle && projectId && (
<div
ref={ref}
className="flex h-full w-full max-w-[21.5rem] flex-shrink-0 flex-col gap-3.5 overflow-y-auto border-l border-custom-border-100 bg-custom-sidebar-background-100 px-4 duration-300 fixed md:relative right-0 z-[9]"
@@ -47,7 +51,13 @@ export const CyclePeekOverview: React.FC<Props> = observer(({ projectId, workspa
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
<CycleDetailsSidebar handleClose={handleClose} isArchived={isArchived} cycleId={peekCycle} />
<CycleDetailsSidebar
handleClose={handleClose}
isArchived={isArchived}
projectId={projectId}
workspaceSlug={workspaceSlug}
cycleId={peekCycle}
/>
</div>
)}
</>
+1
View File
@@ -75,6 +75,7 @@ export const CycleForm: React.FC<Props> = (props) => {
onChange(val);
setActiveProject(val);
}}
multiple={false}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getIndex("cover_image")}
@@ -14,10 +14,9 @@ import { CycleQuickActions } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants
import { CYCLE_STATUS } from "@/constants/cycle";
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "@/constants/event-tracker";
// helpers
import { findHowManyDaysLeft, getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { generateQueryParams } from "@/helpers/router.helper";
@@ -69,11 +68,11 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const cycleStatus = cycleDetails.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft";
const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
EUserPermissionsLevel.PROJECT,
workspaceSlug,
projectId
);
const renderIcon = Boolean(cycleDetails.start_date) || Boolean(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>) => {
@@ -201,9 +200,9 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
router.push(`${pathname}?${query}`);
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
} else {
router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`);
router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false });
}
};
@@ -0,0 +1,44 @@
"use client";
import React, { FC } from "react";
import { observer } from "mobx-react";
import { ChevronRight } from "lucide-react";
// icons
import { Row, Logo } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { useProject } from "@/hooks/store/use-project";
type Props = {
projectId: string;
count?: number;
showCount?: boolean;
isExpanded?: boolean;
};
export const CycleListProjectGroupHeader: FC<Props> = observer((props) => {
const { projectId, count, showCount = false, isExpanded = false } = props;
// store hooks
const { getProjectById } = useProject();
// derived values
const project = getProjectById(projectId);
if (!project) return null;
return (
<Row className="flex items-center gap-2 flex-shrink-0 py-2.5">
<ChevronRight
className={cn("h-4 w-4 text-custom-sidebar-text-300 duration-300 ", {
"rotate-90": isExpanded,
})}
strokeWidth={2}
/>
<div className="flex size-4 flex-shrink-0 items-center justify-center overflow-hidden">
<Logo logo={project.logo_props} size={16} />
</div>
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden">
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{project.name}</div>
{showCount && <div className="pl-2 text-sm font-medium text-custom-text-300">{`${count ?? "0"}`}</div>}
</div>
</Row>
);
});
@@ -4,7 +4,7 @@ import { FC, MouseEvent, useRef } from "react";
import { observer } from "mobx-react";
import { usePathname, useSearchParams } from "next/navigation";
// icons
import { Check, Info } from "lucide-react";
import { Check } from "lucide-react";
// types
import type { TCycleGroups } from "@plane/types";
// ui
@@ -72,9 +72,9 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const query = generateQueryParams(searchParams, ["peekCycle"]);
if (searchParams.has("peekCycle") && searchParams.get("peekCycle") === cycleId) {
router.push(`${pathname}?${query}`);
router.push(`${pathname}?${query}`, {}, { showProgressBar: false });
} else {
router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`);
router.push(`${pathname}?${query && `${query}&`}peekCycle=${cycleId}`, {}, { showProgressBar: false });
}
};
+1
View File
@@ -3,3 +3,4 @@ export * from "./cycles-list-map";
export * from "./root";
export * from "./cycle-list-item-action";
export * from "./cycle-list-group-header";
export * from "./cycle-list-project-group-header";

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