Compare commits

...
Author SHA1 Message Date
vamsikrishnamathala 950db8d7ea Merge branch 'preview' of github.com:makeplane/plane into chore_page-icon-picker 2025-04-25 19:42:30 +05:30
vamsikrishnamathala 1cf32e23b0 chore: set emoji picker visibility to always 2025-04-25 19:41:58 +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
79 changed files with 410 additions and 183 deletions
+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 -9
View File
@@ -54,7 +54,7 @@ x-app-env: &app-env
services:
web:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-frontend:${APP_RELEASE:-stable}
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
@@ -65,7 +65,7 @@ services:
- worker
space:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-space:${APP_RELEASE:-stable}
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
@@ -77,7 +77,7 @@ services:
- web
admin:
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-admin:${APP_RELEASE:-stable}
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
@@ -88,7 +88,7 @@ services:
- web
live:
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-live:${APP_RELEASE:-stable}
command: node live/dist/server.js live
environment:
<<: [*live-env]
@@ -101,7 +101,7 @@ services:
- web
api:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
@@ -117,7 +117,7 @@ services:
- plane-mq
worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
@@ -134,7 +134,7 @@ services:
- plane-mq
beat-worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
@@ -151,7 +151,7 @@ services:
- plane-mq
migrator:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
@@ -213,7 +213,7 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
image: ${DOCKERHUB_USER:-artifacts.plane.so/makeplane}/plane-proxy:${APP_RELEASE:-stable}
ports:
- target: 80
published: ${NGINX_PORT:-80}
+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": {
+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>
</>
);
};
};
@@ -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>
@@ -18,19 +18,14 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
// states
const [isLogoPickerOpen, setIsLogoPickerOpen] = useState(false);
// derived values
const { isContentEditable, logo_props, name, updatePageLogo } = page;
const { isContentEditable, logo_props, updatePageLogo } = page;
const isLogoSelected = !!logo_props?.in_use;
const isTitleEmpty = !name || name.trim() === "";
return (
<>
<div className="h-[48px] flex items-end text-left">
{!isLogoSelected && (
<div
className={cn("opacity-0 group-hover/page-header:opacity-100 transition-all duration-200", {
"opacity-100": isTitleEmpty,
})}
>
<div className={cn("opacity-100 group-hover/page-header:opacity-100 transition-all duration-200")}>
<EmojiIconPicker
isOpen={isLogoPickerOpen}
handleToggle={(val) => setIsLogoPickerOpen(val)}
@@ -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": {