Compare commits

..
Author SHA1 Message Date
seifknaniandGitHub 7a46ac192d [Design]:new Error 404 page design (#6987) 2025-04-29 13:51:50 +05:30
Akshat JainandGitHub f278a284c4 chore: comment out APP_RELEASE variable update in build-branch workflow (#6989) 2025-04-28 17:45:44 +05:30
sriram veeraghanta 2bcf6c76cd chore: remove dockerhub user varible from compose 2025-04-28 16:28:48 +05:30
Akshat JainandGitHub fb3e022042 [INFRA-134] fix: Setup and Swarm scripts for DOCKERHUB_USERNAME #6988 2025-04-28 14:37:51 +05:30
Akshat JainandGitHub e3fbb7b073 [INFRA-134]: Update Docker images to use new artifact repository path #6978 2025-04-25 18:09:43 +05:30
Anmol Singh BhatiaandGitHub cce6dd581c [WEB-3948] chore: recent work item improvement (#6976)
* chore: issue entity data type updated

* chore: HomePeekOverviewsRoot component added

* chore: recent work item improvement and code refactor
2025-04-25 15:08:10 +05:30
Akshita GoyalandGitHub d86ac368a4 [WEB-3863] fix: handled error handling for link editing #6968 2025-04-25 14:31:35 +05:30
101994840a [WEB-3944] fix: Error Toast message content update while uploading images (#6969)
* fix: handled svg uploads

* chore: proper error message with all allowed types

---------

Co-authored-by: sangeethailango <[email protected]>
2025-04-25 14:30:12 +05:30
Anmol Singh BhatiaandGitHub f60f57ef11 [WEB-3494] chore: platform ux copy changes (#6970)
* chore: project quick action option ux copy updated

* chore: project tour copy updated
2025-04-25 14:29:09 +05:30
Prateek ShouryaandGitHub 546217f09b [WEB-3953] fix: issue description assets upload when project id is switched (#6975) 2025-04-25 14:27:40 +05:30
sriram veeraghanta 6df8323665 fix: add gzip upload support 2025-04-24 17:50:37 +05:30
SangeethaandGitHub 77d022df71 [WEB-3919] chore: support .sql file attachment #6966 2025-04-24 17:39:16 +05:30
M. PalanikannanandGitHub 797f150ec4 [WIKI-331] fix: editor ref issues while locking/unlocking page #6965 2025-04-24 17:38:41 +05:30
sriram veeraghanta b54f54999e chore: bump up the package version 2025-04-24 17:37:50 +05:30
SangeethaandGitHub dff176be8f [WEB-3930] chore: set IN_APP as default source value for intake issue (#6963)
* chore: chore: only allow intake issues with source IN_APP to be created

* chore: set IN_APP as default intake issue
2025-04-24 16:25:15 +05:30
2bbaaed3ea [WEB-3918] fix: api tokens is_active (#6941)
* fix: is_active always returning true
chore: formate expired_at to iso date

* Display exact expiration timestamp for API tokens

* chore: remove conversion to iso

* chore: remove unwanted imports

* fix: added timestamp for api token expiry

* fix: handle none value in expired_at

* fix: fix: handle none value in expired_at

* chore: add type hints

* fix: refactor

---------

Co-authored-by: Alaaeddine bousselmi <[email protected]>
Co-authored-by: gakshita <[email protected]>
Co-authored-by: Akshita Goyal <[email protected]>
2025-04-24 01:28:29 +05:30
Prateek ShouryaandGitHub b5ceb94fb2 [WEB-3930] fix: application crash on accessing intake work items (#6958) 2025-04-23 15:12:54 +05:30
alaabousselmiandGitHub feb6243065 docs: document minimum RAM requirement and issue naming conventions (#6954) 2025-04-22 18:00:19 +05:30
Anmol Singh BhatiaandGitHub 5dacba74c9 [WEB-3923] fix: applied filters list #6957 2025-04-22 17:58:16 +05:30
bIaOandGitHub 0efb0c239c feat: improve setup.sh script with better error handling and user feedback (#6758) 2025-04-22 17:56:34 +05:30
Vamsi KrishnaandGitHub c8be836d6c [WEB-3920]fix: estimate activity #6950 2025-04-22 17:45:15 +05:30
NikhilandGitHub 833b82e247 [WEB-3927] chore: add logging to support json logging (#6955)
* chore: update logging to json based logging

* chore: add logging to file
2025-04-22 17:41:58 +05:30
Akshita GoyalandGitHub 280aa7f671 [WEB-3399] fix: progress data for cycle list item #6956 2025-04-22 17:41:06 +05:30
85 changed files with 495 additions and 215 deletions
+1 -1
View File
@@ -273,7 +273,7 @@ jobs:
run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deploy/selfhost/docker-compose.yml
sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
- name: Create Release
id: create_release
+18
View File
@@ -15,6 +15,22 @@ Without said minimal reproduction, we won't be able to investigate all [issues](
You can open a new issue with this [issue form](https://github.com/makeplane/plane/issues/new).
### Naming conventions for issues
When opening a new issue, please use a clear and concise title that follows this format:
- For bugs: `🐛 Bug: [short description]`
- For features: `🚀 Feature: [short description]`
- For improvements: `🛠️ Improvement: [short description]`
- For documentation: `📘 Docs: [short description]`
**Examples:**
- `🐛 Bug: API token expiry time not saving correctly`
- `📘 Docs: Clarify RAM requirement for local setup`
- `🚀 Feature: Allow custom time selection for token expiration`
This helps us triage and manage issues more efficiently.
## Projects setup and Architecture
### Requirements
@@ -23,6 +39,8 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- **Memory**: Minimum **12 GB RAM** recommended
> ⚠️ Running the project on a system with only 8 GB RAM may lead to setup failures or memory crashes (especially during Docker container build/start or dependency install). Use cloud environments like GitHub Codespaces or upgrade local RAM if possible.
### Setup the project
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+2 -1
View File
@@ -20,6 +20,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from plane.utils.host import base_host
from .base import BaseAPIView
from plane.db.models.intake import SourceType
class IntakeIssueAPIEndpoint(BaseAPIView):
@@ -125,7 +126,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
intake_id=intake.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "IN-APP"),
source=SourceType.IN_APP,
)
# Create an Issue Activity
issue_activity.delay(
+9
View File
@@ -1,5 +1,7 @@
from .base import BaseSerializer
from plane.db.models import APIToken, APIActivityLog
from rest_framework import serializers
from django.utils import timezone
class APITokenSerializer(BaseSerializer):
@@ -17,10 +19,17 @@ class APITokenSerializer(BaseSerializer):
class APITokenReadSerializer(BaseSerializer):
is_active = serializers.SerializerMethodField()
class Meta:
model = APIToken
exclude = ("token",)
def get_is_active(self, obj: APIToken) -> bool:
if obj.expired_at is None:
return True
return timezone.now() < obj.expired_at
class APIActivityLogSerializer(BaseSerializer):
class Meta:
+4 -4
View File
@@ -137,7 +137,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -351,7 +351,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -552,7 +552,7 @@ class ProjectAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -683,7 +683,7 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
# For some cases, the bulk api is called after the issue is deleted creating
# an integrity error
try:
assets.update(issue_id=entity_id)
assets.update(issue_id=entity_id, project_id=project_id)
except IntegrityError:
pass
+2 -3
View File
@@ -44,6 +44,7 @@ from plane.app.views.base import BaseAPIView
from plane.utils.timezone_converter import user_timezone_converter
from plane.utils.global_paginator import paginate
from plane.utils.host import base_host
from plane.db.models.intake import SourceType
class IntakeViewSet(BaseViewSet):
@@ -278,7 +279,7 @@ class IntakeIssueViewSet(BaseViewSet):
intake_id=intake_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "IN-APP"),
source=SourceType.IN_APP,
)
# Create an Issue Activity
issue_activity.delay(
@@ -408,7 +409,6 @@ class IntakeIssueViewSet(BaseViewSet):
)
if issue_serializer.is_valid():
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
@@ -607,7 +607,6 @@ class IntakeIssueViewSet(BaseViewSet):
class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
+31 -1
View File
@@ -1,8 +1,16 @@
# Python imports
import os
import logging
# Third party imports
from celery import Celery
from plane.settings.redis import redis_instance
from pythonjsonlogger.jsonlogger import JsonFormatter
from celery.signals import after_setup_logger, after_setup_task_logger
from celery.schedules import crontab
# Module imports
from plane.settings.redis import redis_instance
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@@ -47,6 +55,28 @@ app.conf.beat_schedule = {
},
}
# Setup logging
@after_setup_logger.connect
def setup_loggers(logger, *args, **kwargs):
formatter = JsonFormatter(
'"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
@after_setup_task_logger.connect
def setup_task_loggers(logger, *args, **kwargs):
formatter = JsonFormatter(
'"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
+4
View File
@@ -31,6 +31,10 @@ class Intake(ProjectBaseModel):
ordering = ("name",)
class SourceType(models.TextChoices):
IN_APP = "IN_APP"
class IntakeIssue(ProjectBaseModel):
intake = models.ForeignKey(
"db.Intake", related_name="issue_intake", on_delete=models.CASCADE
@@ -1,40 +0,0 @@
# Module imports
from plane.db.models import APIActivityLog
from plane.utils.ip_address import get_client_ip
class APITokenLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_body = request.body
response = self.get_response(request)
self.process_request(request, response, request_body)
return response
def process_request(self, request, response, request_body):
api_key_header = "X-Api-Key"
api_key = request.headers.get(api_key_header)
# If the API key is present, log the request
if api_key:
try:
APIActivityLog.objects.create(
token_identifier=api_key,
path=request.path,
method=request.method,
query_params=request.META.get("QUERY_STRING", ""),
headers=str(request.headers),
body=(request_body.decode("utf-8") if request_body else None),
response_body=(
response.content.decode("utf-8") if response.content else None
),
response_code=response.status_code,
ip_address=get_client_ip(request=request),
user_agent=request.META.get("HTTP_USER_AGENT", None),
)
except Exception as e:
print(e)
# If the token does not exist, you can decide whether to log this as an invalid attempt
return None
+41 -1
View File
@@ -10,8 +10,10 @@ from rest_framework.request import Request
# Module imports
from plane.utils.ip_address import get_client_ip
from plane.db.models import APIActivityLog
api_logger = logging.getLogger("plane.api")
api_logger = logging.getLogger("plane.api.request")
class RequestLoggerMiddleware:
@@ -69,3 +71,41 @@ class RequestLoggerMiddleware:
# return the response
return response
class APITokenLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_body = request.body
response = self.get_response(request)
self.process_request(request, response, request_body)
return response
def process_request(self, request, response, request_body):
api_key_header = "X-Api-Key"
api_key = request.headers.get(api_key_header)
# If the API key is present, log the request
if api_key:
try:
APIActivityLog.objects.create(
token_identifier=api_key,
path=request.path,
method=request.method,
query_params=request.META.get("QUERY_STRING", ""),
headers=str(request.headers),
body=(request_body.decode("utf-8") if request_body else None),
response_body=(
response.content.decode("utf-8") if response.content else None
),
response_code=response.status_code,
ip_address=get_client_ip(request=request),
user_agent=request.META.get("HTTP_USER_AGENT", None),
)
except Exception as e:
api_logger.exception(e)
# If the token does not exist, you can decide whether to log this as an invalid attempt
return None
+5 -1
View File
@@ -58,7 +58,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"crum.CurrentRequestUserMiddleware",
"django.middleware.gzip.GZipMiddleware",
"plane.middleware.api_log_middleware.APITokenLogMiddleware",
"plane.middleware.logger.APITokenLogMiddleware",
"plane.middleware.logger.RequestLoggerMiddleware",
]
@@ -391,4 +391,8 @@ ATTACHMENT_MIME_TYPES = [
"text/xml",
"text/csv",
"application/xml",
# SQL
"application/x-sql",
# Gzip
"application/x-gzip",
]
+10
View File
@@ -56,6 +56,11 @@ LOGGING = {
}
},
"loggers": {
"plane.api.request": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane.api": {"level": "INFO", "handlers": ["console"], "propagate": False},
"plane.worker": {"level": "INFO", "handlers": ["console"], "propagate": False},
"plane.exception": {
@@ -63,5 +68,10 @@ LOGGING = {
"handlers": ["console"],
"propagate": False,
},
"plane.external": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}
+10
View File
@@ -58,6 +58,11 @@ LOGGING = {
},
},
"loggers": {
"plane.api.request": {
"level": "DEBUG" if DEBUG else "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane.api": {
"level": "DEBUG" if DEBUG else "INFO",
"handlers": ["console"],
@@ -70,6 +75,11 @@ LOGGING = {
},
"plane.exception": {
"level": "DEBUG" if DEBUG else "ERROR",
"handlers": ["console", "file"],
"propagate": False,
},
"plane.external": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
+1 -1
View File
@@ -96,7 +96,7 @@ class EntityAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
+1 -1
View File
@@ -9,7 +9,7 @@ from django.conf import settings
def log_exception(e):
# Log the error
logger = logging.getLogger("plane.exception")
logger.error(str(e))
logger.exception(e)
if settings.DEBUG:
# Print the traceback if in debug mode
+9 -10
View File
@@ -51,10 +51,9 @@ x-app-env: &app-env
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
services:
web:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable}
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
@@ -65,7 +64,7 @@ services:
- worker
space:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable}
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
@@ -77,7 +76,7 @@ services:
- web
admin:
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable}
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
@@ -88,7 +87,7 @@ services:
- web
live:
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable}
command: node live/dist/server.js live
environment:
<<: [*live-env]
@@ -101,7 +100,7 @@ services:
- web
api:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
@@ -117,7 +116,7 @@ services:
- plane-mq
worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
@@ -134,7 +133,7 @@ services:
- plane-mq
beat-worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
@@ -151,7 +150,7 @@ services:
- plane-mq
migrator:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
@@ -213,7 +212,7 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable}
ports:
- target: 80
published: ${NGINX_PORT:-80}
+2 -2
View File
@@ -5,7 +5,7 @@ SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE=stable
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@@ -631,7 +631,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
+2 -2
View File
@@ -5,7 +5,7 @@ SERVICE_FOLDER=plane-app
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@@ -596,7 +596,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
+1 -1
View File
@@ -60,4 +60,4 @@ GUNICORN_WORKERS=1
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"
API_KEY_RATE_LIMIT=60/minute
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.25.3",
"version": "0.26.0",
"private": true,
"main": "./src/index.ts",
"license": "AGPL-3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.25.3",
"version": "0.26.0",
"description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"files": [
"library.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally",
"private": true,
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Nepodařilo se odstranit projekt z oblíbených. Zkuste to prosím znovu.",
"add_to_favorites": "Přidat do oblíbených",
"remove_from_favorites": "Odebrat z oblíbených",
"publish_settings": "Nastavení publikování",
"publish_project": "Publikovat projekt",
"publish": "Publikovat",
"copy_link": "Kopírovat odkaz",
"leave_project": "Opustit projekt",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Projekt konnte nicht aus den Favoriten entfernt werden. Bitte versuchen Sie es erneut.",
"add_to_favorites": "Zu Favoriten hinzufügen",
"remove_from_favorites": "Aus Favoriten entfernen",
"publish_settings": "Veröffentlichungseinstellungen",
"publish_project": "Projekt veröffentlichen",
"publish": "Veröffentlichen",
"copy_link": "Link kopieren",
"leave_project": "Projekt verlassen",
@@ -180,7 +180,7 @@
"couldnt_remove_the_project_from_favorites": "Couldn't remove the project from favorites. Please try again.",
"add_to_favorites": "Add to favorites",
"remove_from_favorites": "Remove from favorites",
"publish_settings": "Publish settings",
"publish_project": "Publish project",
"publish": "Publish",
"copy_link": "Copy link",
"leave_project": "Leave project",
@@ -352,7 +352,7 @@
"couldnt_remove_the_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.",
"add_to_favorites": "Agregar a favoritos",
"remove_from_favorites": "Eliminar de favoritos",
"publish_settings": "Configuración de publicación",
"publish_project": "Publicar proyecto",
"publish": "Publicar",
"copy_link": "Copiar enlace",
"leave_project": "Abandonar proyecto",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.",
"add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Supprimer des favoris",
"publish_settings": "Paramètres de publication",
"publish_project": "Publier le projet",
"publish": "Publier",
"copy_link": "Copier le lien",
"leave_project": "Quitter le projet",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "Tidak dapat menghapus proyek dari favorit. Silakan coba lagi.",
"add_to_favorites": "Tambah ke favorit",
"remove_from_favorites": "Hapus dari favorit",
"publish_settings": "Pengaturan publikasi",
"publish_project": "Publikasikan proyek",
"publish": "Publikasikan",
"copy_link": "Salin tautan",
"leave_project": "Tinggalkan proyek",
@@ -349,7 +349,7 @@
"couldnt_remove_the_project_from_favorites": "Impossibile rimuovere il progetto dai preferiti. Per favore, riprova.",
"add_to_favorites": "Aggiungi ai preferiti",
"remove_from_favorites": "Rimuovi dai preferiti",
"publish_settings": "Impostazioni di pubblicazione",
"publish_project": "Pubblica progetto",
"publish": "Pubblica",
"copy_link": "Copia link",
"leave_project": "Lascia progetto",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "プロジェクトをお気に入りから削除できませんでした。もう一度お試しください。",
"add_to_favorites": "お気に入りに追加",
"remove_from_favorites": "お気に入りから削除",
"publish_settings": "公開設定",
"publish_project": "プロジェクトを公開",
"publish": "公開",
"copy_link": "リンクをコピー",
"leave_project": "プロジェクトを退出",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "프로젝트를 즐겨찾기에서 제거하지 못했습니다. 다시 시도해주세요.",
"add_to_favorites": "즐겨찾기에 추가",
"remove_from_favorites": "즐겨찾기에서 제거",
"publish_settings": "설정 게시",
"publish_project": "프로젝트 게시",
"publish": "게시",
"copy_link": "링크 복사",
"leave_project": "프로젝트 떠나기",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Nie udało się usunąć projektu z ulubionych. Spróbuj ponownie.",
"add_to_favorites": "Dodaj do ulubionych",
"remove_from_favorites": "Usuń z ulubionych",
"publish_settings": "Ustawienia publikowania",
"publish_project": "Opublikuj projekt",
"publish": "Opublikuj",
"copy_link": "Kopiuj link",
"leave_project": "Opuść projekt",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "Não foi possível remover o projeto dos favoritos. Por favor, tente novamente.",
"add_to_favorites": "Adicionar aos favoritos",
"remove_from_favorites": "Remover dos favoritos",
"publish_settings": "Configurações de publicação",
"publish_project": "Publicar projeto",
"publish": "Publicar",
"copy_link": "Copiar link",
"leave_project": "Sair do projeto",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "Nu s-a putut elimina proiectul din favorite. Încearcă din nou.",
"add_to_favorites": "Adaugă la favorite",
"remove_from_favorites": "Elimină din favorite",
"publish_settings": "Setări de publicare",
"publish_project": "Publică proiectul",
"publish": "Publică",
"copy_link": "Copiază link-ul",
"leave_project": "Părăsește proiectul",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Не удалось удалить проект из избранного. Попробуйте снова.",
"add_to_favorites": "Добавить в избранное",
"remove_from_favorites": "Удалить из избранного",
"publish_settings": "Настройки публикации",
"publish_project": "Опубликовать проект",
"publish": "Опубликовать",
"copy_link": "Копировать ссылку",
"leave_project": "Покинуть проект",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Nepodarilo sa odstrániť projekt z obľúbených. Skúste to prosím znova.",
"add_to_favorites": "Pridať do obľúbených",
"remove_from_favorites": "Odstrániť z obľúbených",
"publish_settings": "Nastavenia publikovania",
"publish_project": "Publikovať projekt",
"publish": "Publikovať",
"copy_link": "Kopírovať odkaz",
"leave_project": "Opustiť projekt",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "Proje favorilerden kaldırılamadı. Lütfen tekrar deneyin.",
"add_to_favorites": "Favorilere ekle",
"remove_from_favorites": "Favorilerden kaldır",
"publish_settings": "Yayınlama ayarları",
"publish_project": "Projeyi yayımla",
"publish": "Yayınla",
"copy_link": "Bağlantıyı kopyala",
"leave_project": "Projeden ayrıl",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Не вдалося вилучити проєкт із вибраного. Спробуйте ще раз.",
"add_to_favorites": "Додати у вибране",
"remove_from_favorites": "Вилучити з вибраного",
"publish_settings": "Налаштування публікації",
"publish_project": "Опублікувати проєкт",
"publish": "Опублікувати",
"copy_link": "Скопіювати посилання",
"leave_project": "Вийти з проєкту",
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Không thể xóa dự án khỏi mục yêu thích. Vui lòng thử lại.",
"add_to_favorites": "Thêm vào mục yêu thích",
"remove_from_favorites": "Xóa khỏi mục yêu thích",
"publish_settings": "Cài đặt xuất bản",
"publish_project": "Xuất bản dự án",
"publish": "Xuất bản",
"copy_link": "Sao chép liên kết",
"leave_project": "Rời dự án",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "无法从收藏中移除项目。请重试。",
"add_to_favorites": "添加到收藏",
"remove_from_favorites": "从收藏中移除",
"publish_settings": "发布设置",
"publish_project": "发布项目",
"publish": "发布",
"copy_link": "复制链接",
"leave_project": "离开项目",
@@ -350,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "無法從我的最愛移除專案。請再試一次。",
"add_to_favorites": "加入我的最愛",
"remove_from_favorites": "從我的最愛移除",
"publish_settings": "發布設定",
"publish_project": "發佈專案",
"publish": "發布",
"copy_link": "複製連結",
"leave_project": "離開專案",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/logger",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "Logger shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "0.25.3",
"version": "0.26.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"types": "./src/index.d.ts",
+1
View File
@@ -35,6 +35,7 @@ export type TIssueEntityData = {
sequence_id: number;
project_id: string;
project_identifier: string;
is_epic: boolean;
};
export type TActivityEntityData = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"files": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.25.3",
"version": "0.26.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "0.25.3",
"version": "0.26.0",
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true,
+83 -9
View File
@@ -1,15 +1,89 @@
#!/bin/bash
cp ./.env.example ./.env
# Export for tr error in mac
# Plane Project Setup Script
# This script prepares the local development environment by setting up all necessary .env files
# https://github.com/makeplane/plane
# Set colors for output messages
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Print header
echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD}${BLUE} Plane - Project Management Tool ${NC}"
echo -e "${BOLD}${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD}Setting up your development environment...${NC}\n"
# Function to handle file copying with error checking
copy_env_file() {
local source=$1
local destination=$2
if [ ! -f "$source" ]; then
echo -e "${RED}Error: Source file $source does not exist.${NC}"
return 1
fi
cp "$source" "$destination"
if [ $? -eq 0 ]; then
echo -e "${GREEN}${NC} Copied $destination"
else
echo -e "${RED}${NC} Failed to copy $destination"
return 1
fi
}
# Export character encoding settings for macOS compatibility
export LC_ALL=C
export LC_CTYPE=C
echo -e "${YELLOW}Setting up environment files...${NC}"
cp ./web/.env.example ./web/.env
cp ./apiserver/.env.example ./apiserver/.env
cp ./space/.env.example ./space/.env
cp ./admin/.env.example ./admin/.env
cp ./live/.env.example ./live/.env
# Copy all environment example files
services=("" "web" "apiserver" "space" "admin" "live")
success=true
# Generate the SECRET_KEY that will be used by django
echo -e "\nSECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
for service in "${services[@]}"; do
prefix="./"
if [ "$service" != "" ]; then
prefix="./$service/"
fi
copy_env_file "${prefix}.env.example" "${prefix}.env" || success=false
done
# Generate SECRET_KEY for Django
if [ -f "./apiserver/.env" ]; then
echo -e "\n${YELLOW}Generating Django SECRET_KEY...${NC}"
SECRET_KEY=$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)
if [ -z "$SECRET_KEY" ]; then
echo -e "${RED}Error: Failed to generate SECRET_KEY.${NC}"
echo -e "${RED}Ensure 'tr' and 'head' commands are available on your system.${NC}"
success=false
else
echo -e "SECRET_KEY=\"$SECRET_KEY\"" >> ./apiserver/.env
echo -e "${GREEN}${NC} Added SECRET_KEY to apiserver/.env"
fi
else
echo -e "${RED}${NC} apiserver/.env not found. SECRET_KEY not added."
success=false
fi
# Summary
echo -e "\n${YELLOW}Setup status:${NC}"
if [ "$success" = true ]; then
echo -e "${GREEN}${NC} Environment setup completed successfully!\n"
echo -e "${BOLD}Next steps:${NC}"
echo -e "1. Review the .env files in each folder if needed"
echo -e "2. Start the services with: ${BOLD}docker compose -f docker-compose-local.yml up -d${NC}"
echo -e "\n${GREEN}Happy coding! 🚀${NC}"
else
echo -e "${RED}${NC} Some issues occurred during setup. Please check the errors above.\n"
echo -e "For help, visit: ${BLUE}https://github.com/makeplane/plane${NC}"
exit 1
fi
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.25.3",
"version": "0.26.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+77 -20
View File
@@ -4,39 +4,96 @@ import React from "react";
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { useTranslation } from "@plane/i18n";
import { useEventTracker } from "@/hooks/store";
import { NAVIGATE_TO_SIGNUP } from "@plane/constants";
// ui
import { Button } from "@plane/ui";
import { useTheme } from "next-themes";
// images
import Image404 from "@/public/404.svg";
import Image404 from "@/public/new404.png";
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
export const metadata: Metadata = {
title: "404 - Page Not Found",
};
const PageNotFound = () => (
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<Image src={Image404} layout="fill" alt="404- Page not found" />
const PageNotFound = (() => {
const { resolvedTheme } = useTheme();
// plane hooks
const { t } = useTranslation();
// hooks
const { captureEvent } = useEventTracker();
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<div className="h-screen w-full overflow-hidden bg-custom-background-100 flex flex-col">
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
{/* Top Navigation */}
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Link href="/" className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
{t("auth.common.new_to_plane")}
<Link
href="/sign-up"
onClick={() => captureEvent(NAVIGATE_TO_SIGNUP, {})}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}
</Link>
</div>
</div>
{/* 404 Content */}
<div className="grid h-full items-start justify-center pt-24 p-4 z-10">
<div className="space-y-8 text-center">
{/* Image */}.
<div className="mx-auto h-200 w-80 lg:h-80 lg:w-100 relative">
<Image
src={Image404}
alt="404 - Page not found"
fill
className="object-contain"
priority
/>
</div>
<div className="space-y-2 relative z-10">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
</div>
</div>
<Link href="/">
<span className="flex justify-center">
<Button variant="neutral-primary" size="md">
Go to Home
</Button>
</span>
</Link>
</div>
</div>
</div>
);
});
export default PageNotFound;
+1
View File
@@ -0,0 +1 @@
export * from "./peek-overviews";
@@ -0,0 +1,9 @@
"use client";
import { IssuePeekOverview } from "@/components/issues";
export const HomePeekOverviewsRoot = () => (
<>
<IssuePeekOverview />
</>
);
@@ -46,7 +46,7 @@ export const CreateApiTokenModal: React.FC<Props> = (props) => {
const csvData = {
Title: data.label,
Description: data.description,
Expiry: data.expired_at ? renderFormattedDate(data.expired_at)?.replace(",", " ") ?? "" : "Never expires",
Expiry: data.expired_at ? (renderFormattedDate(data.expired_at)?.replace(",", " ") ?? "") : "Never expires",
"Secret key": data.token ?? "",
};
+20 -16
View File
@@ -13,7 +13,7 @@ import { Button, CustomSelect, Input, TextArea, ToggleSwitch, TOAST_TYPE, setToa
import { DateDropdown } from "@/components/dropdowns";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
type Props = {
handleClose: () => void;
@@ -51,20 +51,21 @@ const defaultValues: Partial<IApiToken> = {
expired_at: null,
};
const getExpiryDate = (val: string): string | null | undefined => {
const getExpiryDate = (val: string): Date | null | undefined => {
const today = new Date();
const dateToAdd = EXPIRY_DATE_OPTIONS.find((option) => option.key === val)?.value;
if (dateToAdd) {
const expiryDate = add(today, dateToAdd);
return renderFormattedDate(expiryDate);
}
if (dateToAdd) return add(today, dateToAdd);
return null;
};
const getFormattedDate = (date: Date): Date => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const seconds = now.getSeconds();
return add(date, { hours, minutes, seconds });
};
export const CreateApiTokenForm: React.FC<Props> = (props) => {
const { handleClose, neverExpires, toggleNeverExpires, onSubmit } = props;
// states
@@ -97,12 +98,13 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
// if never expires is toggled on, set expired_at to null
if (neverExpires) payload.expired_at = null;
// if never expires is toggled off, and the user has selected a custom date, set expired_at to the custom date
else if (data.expired_at === "custom") payload.expired_at = renderFormattedPayloadDate(customDate);
else if (data.expired_at === "custom") {
payload.expired_at = customDate && getFormattedDate(customDate).toISOString();
}
// if never expires is toggled off, and the user has selected a predefined date, set expired_at to the predefined date
else {
const expiryDate = getExpiryDate(data.expired_at ?? "");
if (expiryDate) payload.expired_at = renderFormattedPayloadDate(new Date(expiryDate));
if (expiryDate) payload.expired_at = expiryDate.toISOString();
}
await onSubmit(payload).then(() => {
@@ -114,6 +116,8 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
const today = new Date();
const tomorrow = add(today, { days: 1 });
const expiredAt = watch("expired_at");
const expiryDate = getExpiryDate(expiredAt ?? "");
const customDateFormatted = customDate && getFormattedDate(customDate);
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -219,10 +223,10 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
<span className="text-xs text-custom-text-400">
{expiredAt === "custom"
? customDate
? `Expires ${renderFormattedDate(customDate)}`
? `Expires ${renderFormattedDate(customDateFormatted ?? "")} at ${renderFormattedTime(customDateFormatted ?? "")}`
: null
: expiredAt
? `Expires ${getExpiryDate(expiredAt ?? "")}`
? `Expires ${renderFormattedDate(expiryDate ?? "")} at ${renderFormattedTime(expiryDate ?? "")}`
: null}
</span>
)}
@@ -249,4 +253,4 @@ export const CreateApiTokenForm: React.FC<Props> = (props) => {
</div>
</form>
);
};
};
@@ -6,7 +6,7 @@ import { IApiToken } from "@plane/types";
// ui
import { Button, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
import { copyTextToClipboard } from "@/helpers/string.helper";
// types
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -49,7 +49,9 @@ export const GeneratedTokenDetails: React.FC<Props> = (props) => {
</button>
<div className="mt-6 flex items-center justify-between">
<p className="text-xs text-custom-text-400">
{tokenDetails.expired_at ? `Expires ${renderFormattedDate(tokenDetails.expired_at)}` : "Never expires"}
{tokenDetails.expired_at
? `Expires ${renderFormattedDate(tokenDetails.expired_at!)} at ${renderFormattedTime(tokenDetails.expired_at!)}`
: "Never expires"}
</p>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
{t("close")}
@@ -6,7 +6,7 @@ import { IApiToken } from "@plane/types";
// components
import { Tooltip } from "@plane/ui";
import { DeleteApiTokenModal } from "@/components/api-token";
import { renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper";
import { renderFormattedDate, calculateTimeAgo, renderFormattedTime } from "@/helpers/date-time.helper";
import { usePlatformOS } from "@/hooks/use-platform-os";
// ui
// helpers
@@ -52,7 +52,7 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
<p className="mb-1 text-xs leading-6 text-custom-text-400">
{token.is_active
? token.expired_at
? `Expires ${renderFormattedDate(token.expired_at!)}`
? `Expires ${renderFormattedDate(token.expired_at!)} at ${renderFormattedTime(token.expired_at!)}`
: "Never expires"
: `Expired ${calculateTimeAgo(token.expired_at)}`}
</p>
@@ -60,4 +60,4 @@ export const ApiTokenListItem: React.FC<Props> = (props) => {
</div>
</>
);
};
};
+4 -12
View File
@@ -139,15 +139,7 @@ export const CommentCard: FC<TCommentCard> = observer((props) => {
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
<div
onKeyDown={(e) => {
if (
e.key === "Enter" &&
!e.shiftKey &&
!e.ctrlKey &&
!e.metaKey &&
!isEmpty &&
editorRef.current?.isEditorReadyToDiscard()
)
handleSubmit(onEnter)(e);
if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.metaKey && !isEmpty) handleSubmit(onEnter)(e);
}}
>
<LiteTextEditor
@@ -169,9 +161,9 @@ export const CommentCard: FC<TCommentCard> = observer((props) => {
return asset_id;
}}
projectId={projectId?.toString() ?? ""}
editorClassName="[&>*]:!py-0 [&_*]:!text-sm"
editorClassName="[&>*]:!py-0 [&>*]:!text-sm"
parentClassName="p-2"
/>
/>
</div>
<div className="flex gap-1 self-end">
{!isEmpty && (
@@ -216,7 +208,7 @@ export const CommentCard: FC<TCommentCard> = observer((props) => {
initialValue={comment.comment_html ?? ""}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
editorClassName="[&>*]:!py-0 [&_*]:!text-sm"
editorClassName="[&>*]:!py-0 [&>*]:!text-sm"
containerClassName="!py-1"
projectId={(projectId as string) ?? ""}
/>
@@ -125,7 +125,7 @@ export const CommentCreate: FC<TCommentCreate> = observer((props) => {
}}
ref={editorRef}
initialValue={value ?? "<p></p>"}
containerClassName="min-h-min [&_p]:!p-0 [&_*]:!text-sm"
containerClassName="min-h-min [&_p]:!p-0 [&_p]:!text-sm"
onChange={(comment_json, comment_html) => onChange(comment_html)}
accessSpecifier={accessValue ?? EIssueCommentAccessSpecifier.INTERNAL}
handleAccessChange={onAccessChange}
@@ -14,7 +14,7 @@ import { useOutsideClickDetector } from "@plane/hooks";
// plane types
import { EFileAssetType } from "@plane/types/src/enums";
// ui
import { Button, Input, Loader } from "@plane/ui";
import { Button, Input, Loader, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
@@ -114,7 +114,16 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
},
image
)
.then((res) => uploadCallback(res.asset_url));
.then((res) => uploadCallback(res.asset_url))
.catch((error) => {
console.error("Error uploading user cover image:", error);
setIsImageUploading(false);
setToast({
message: error?.error ?? "The image could not be uploaded",
type: TOAST_TYPE.ERROR,
title: "Image not uploaded",
});
});
} else {
if (!workspaceSlug) return;
await fileService
@@ -126,7 +135,16 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
},
image
)
.then((res) => uploadCallback(res.asset_url));
.then((res) => uploadCallback(res.asset_url))
.catch((error) => {
console.error("Error uploading project cover image:", error);
setIsImageUploading(false);
setToast({
message: error?.error ?? "The image could not be uploaded",
type: TOAST_TYPE.ERROR,
title: "Image not uploaded",
});
});
}
};
@@ -55,14 +55,8 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const isCompleted = cycleStatus === "completed";
const isActive = cycleStatus === "current";
const cycleTotalIssues =
cycleDetails.backlog_issues +
cycleDetails.unstarted_issues +
cycleDetails.started_issues +
cycleDetails.completed_issues +
cycleDetails.cancelled_issues;
const completionPercentage = (cycleDetails.completed_issues / cycleTotalIssues) * 100;
const completionPercentage =
((cycleDetails.completed_issues + cycleDetails.cancelled_issues) / cycleDetails.total_issues) * 100;
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
+2 -2
View File
@@ -12,7 +12,7 @@ import { cn } from "@/helpers/common.helper";
import { useUserProfile, useEventTracker, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
import { IssuePeekOverview } from "../issues";
import { HomePeekOverviewsRoot } from "@/plane-web/components/home";
import { DashboardWidgets } from "./home-dashboard-widgets";
import { UserGreetingsView } from "./user-greetings";
@@ -57,7 +57,7 @@ export const WorkspaceHomeView = observer(() => {
</div>
)}
<>
<IssuePeekOverview />
<HomePeekOverviewsRoot />
<ContentWrapper
className={cn("gap-6 bg-custom-background-90/20", {
"vertical-scrollbar scrollbar-lg": windowWidth >= 768,
@@ -1,3 +1,6 @@
import { observer } from "mobx-react";
// plane constants
import { EIssueServiceType } from "@plane/constants";
// plane types
import { TActivityEntityData, TIssueEntityData } from "@plane/types";
// plane ui
@@ -18,11 +21,12 @@ type BlockProps = {
ref: React.RefObject<HTMLDivElement>;
workspaceSlug: string;
};
export const RecentIssue = (props: BlockProps) => {
export const RecentIssue = observer((props: BlockProps) => {
const { activity, ref, workspaceSlug } = props;
// hooks
const { getStateById } = useProjectState();
const { setPeekIssue } = useIssueDetail();
const { setPeekIssue: setPeekEpic } = useIssueDetail(EIssueServiceType.EPICS);
const { getProjectIdentifierById } = useProject();
// derived values
const issueDetails: TIssueEntityData = activity.entity_data as TIssueEntityData;
@@ -38,8 +42,21 @@ export const RecentIssue = (props: BlockProps) => {
issueId: issueDetails?.id,
projectIdentifier,
sequenceId: issueDetails?.sequence_id,
isEpic: issueDetails?.is_epic,
});
const handlePeekOverview = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.stopPropagation();
const peekDetails = {
workspaceSlug,
projectId: issueDetails?.project_id,
issueId: activity.entity_data.id,
};
if (issueDetails?.is_epic) setPeekEpic(peekDetails);
else setPeekIssue(peekDetails);
};
return (
<ListItem
key={activity.id}
@@ -109,12 +126,8 @@ export const RecentIssue = (props: BlockProps) => {
disableLink={false}
className="bg-transparent my-auto !px-2 border-none py-3"
itemClassName="my-auto"
onItemClick={(e) => {
e.preventDefault();
e.stopPropagation();
setPeekIssue({ workspaceSlug, projectId: issueDetails?.project_id, issueId: activity.entity_data.id });
}}
onItemClick={handlePeekOverview}
preventDefaultNProgress
/>
);
};
});
@@ -42,5 +42,6 @@ export const InboxStatusIcon = ({
}) => {
if (type === undefined) return null;
const Icon = ICON_PROPERTIES[type];
if (!Icon) return null;
return <Icon.icon size={size} className={cn(`w-3 h-3 ${renderColor && Icon?.textColor(false)}`, className)} />;
};
@@ -39,7 +39,7 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
// hooks
const {
activity: { getActivityById },
comment: { },
comment: {},
} = useIssueDetail();
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const activityRelations = getValidKeysFromObject(ISSUE_RELATION_OPTIONS);
@@ -62,6 +62,7 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
return <IssuePriorityActivity {...componentDefaultProps} showIssue={false} />;
case "estimate_points":
case "estimate_categories":
case "estimate_point" /* This case is to handle all the older recorded activities for estimates. Field changed from "estimate_point" -> `estimate_${estimate_type}`*/:
return <IssueEstimateActivity {...componentDefaultProps} showIssue={false} />;
case "parent":
return <IssueParentActivity {...componentDefaultProps} showIssue={false} />;
@@ -81,6 +81,7 @@ export const ProjectAppliedFiltersRoot: React.FC<TProjectAppliedFiltersRootProps
handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []}
states={projectStates}
alwaysAllowEditing
/>
</Header.LeftItem>
<Header.RightItem>
@@ -132,10 +132,14 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
};
const addIssueToModule = async (issue: TIssue, moduleIds: string[]) => {
if (!workspaceSlug || !activeProjectId) return;
if (!workspaceSlug || !issue.project_id) return;
await issues.changeModulesInIssue(workspaceSlug.toString(), activeProjectId, issue.id, moduleIds, []);
moduleIds.forEach((moduleId) => fetchModuleDetails(workspaceSlug.toString(), activeProjectId, moduleId));
await Promise.all([
issues.changeModulesInIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds, []),
...moduleIds.map(
(moduleId) => issue.project_id && fetchModuleDetails(workspaceSlug.toString(), issue.project_id, moduleId)
),
]);
};
const handleCreateMoreToggleChange = (value: boolean) => {
@@ -182,7 +186,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
if (uploadedAssetIds.length > 0) {
await fileService.updateBulkProjectAssetsUploadStatus(
workspaceSlug?.toString() ?? "",
activeProjectId ?? "",
response?.project_id ?? "",
response?.id ?? "",
{
asset_ids: uploadedAssetIds,
+6 -6
View File
@@ -26,7 +26,7 @@ type Props = {
onComplete: () => void;
};
export type TTourSteps = "welcome" | "issues" | "cycles" | "modules" | "views" | "pages";
export type TTourSteps = "welcome" | "work-items" | "cycles" | "modules" | "views" | "pages";
const TOUR_STEPS: {
key: TTourSteps;
@@ -37,10 +37,10 @@ const TOUR_STEPS: {
nextStep?: TTourSteps;
}[] = [
{
key: "issues",
key: "work-items",
title: "Plan with work items",
description:
"The work item is the building block of the Plane. Most concepts in Plane are either associated with issues and their properties.",
"The work item is the building block of the Plane. Most concepts in Plane are either associated with work items and their properties.",
image: IssuesTour,
nextStep: "cycles",
},
@@ -50,7 +50,7 @@ const TOUR_STEPS: {
description:
"Cycles help you and your team to progress faster, similar to the sprints commonly used in agile development.",
image: CyclesTour,
prevStep: "issues",
prevStep: "work-items",
nextStep: "modules",
},
{
@@ -113,7 +113,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
variant="primary"
onClick={() => {
captureEvent(PRODUCT_TOUR_STARTED);
setStep("issues");
setStep("work-items");
}}
>
Take a Product Tour
@@ -162,7 +162,7 @@ export const TourRoot: React.FC<Props> = observer((props) => {
</Button>
)}
{currentStep?.nextStep && (
<Button variant="primary" onClick={() => setStep(currentStep.nextStep ?? "issues")}>
<Button variant="primary" onClick={() => setStep(currentStep.nextStep ?? "work-items")}>
Next
</Button>
)}
@@ -8,26 +8,32 @@ import { TTourSteps } from "./root";
const sidebarOptions: {
key: TTourSteps;
label: string;
Icon: any;
}[] = [
{
key: "issues",
key: "work-items",
label: "Work items",
Icon: LayersIcon,
},
{
key: "cycles",
label: "Cycles",
Icon: ContrastIcon,
},
{
key: "modules",
label: "Modules",
Icon: DiceIcon,
},
{
key: "views",
label: "Views",
Icon: Layers,
},
{
key: "pages",
label: "Pages",
Icon: FileText,
},
];
@@ -57,7 +63,7 @@ export const TourSidebar: React.FC<Props> = ({ step, setStep }) => (
role="button"
>
<option.Icon className="h-4 w-4" aria-hidden="true" />
{option.key}
{option.label}
</h5>
))}
</div>
@@ -74,6 +74,12 @@ export const PageRoot = observer((props: TPageRootProps) => {
[page.editorRef, setEditorRef]
);
useEffect(() => {
setTimeout(() => {
setEditorRef(editorRef.current);
}, 0);
}, [isContentEditable, setEditorRef]);
const version = searchParams.get("version");
useEffect(() => {
if (!version) {
@@ -332,7 +332,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
<div className="flex h-4 w-4 cursor-pointer items-center justify-center rounded text-custom-sidebar-text-200 transition-all duration-300 hover:bg-custom-sidebar-background-80">
<Share2 className="h-3.5 w-3.5 stroke-[1.5]" />
</div>
<div>{t("publish_settings")}</div>
<div>{t("publish_project")}</div>
</div>
</CustomMenu.MenuItem>
)}
@@ -121,17 +121,28 @@ export class IssueLinkStore implements IIssueLinkStore {
linkId: string,
data: Partial<TIssueLink>
) => {
runInAction(() => {
Object.keys(data).forEach((key) => {
set(this.linkMap, [linkId, key], data[key as keyof TIssueLink]);
const initialData = { ...this.linkMap[linkId] };
try {
runInAction(() => {
Object.keys(data).forEach((key) => {
set(this.linkMap, [linkId, key], data[key as keyof TIssueLink]);
});
});
});
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
const response = await this.issueService.updateIssueLink(workspaceSlug, projectId, issueId, linkId, data);
// fetching activity
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
return response;
// fetching activity
this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId);
return response;
} catch (error) {
console.error("error", error);
runInAction(() => {
Object.keys(initialData).forEach((key) => {
set(this.linkMap, [linkId, key], initialData[key as keyof TIssueLink]);
});
});
return initialData;
}
};
removeLink = async (workspaceSlug: string, projectId: string, issueId: string, linkId: string) => {
+1 -1
View File
@@ -250,10 +250,10 @@ export class ProjectPageStore implements IProjectPageStore {
});
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
const pageInstance = page?.id ? this.getPageById(page.id) : undefined;
runInAction(() => {
if (page?.id) {
const pageInstance = this.getPageById(page.id);
if (pageInstance) {
pageInstance.mutateProperties(page, false);
} else {
+1
View File
@@ -0,0 +1 @@
export * from "ce/components/home";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.25.3",
"version": "0.26.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 779 KiB