release: v0.25.3 #6788

This commit is contained in:
sriram veeraghanta 2025-03-21 17:26:55 +05:30 committed by GitHub
commit 45e25ce18b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 15068 additions and 766 deletions

View file

@ -47,12 +47,6 @@ jobs:
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
@ -123,46 +117,7 @@ jobs:
name: Checkout Files
uses: actions/checkout@v4
- name: Get changed files
id: changed_files
uses: tj-actions/changed-files@v42
with:
files_yaml: |
apiserver:
- apiserver/**
proxy:
- nginx/**
admin:
- admin/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
space:
- space/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
web:
- web/**
- packages/**
- "package.json"
- "yarn.lock"
- "tsconfig.json"
- "turbo.json"
live:
- live/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@ -185,7 +140,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@ -208,7 +162,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@ -231,7 +184,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@ -254,7 +206,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]
@ -277,7 +228,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04
needs: [branch_build_setup]

View file

@ -6,49 +6,9 @@ on:
types: ["opened", "synchronize", "ready_for_review"]
jobs:
get-changed-files:
lint-apiserver:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files_yaml: |
apiserver:
- apiserver/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
space:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
lint-apiserver:
needs: get-changed-files
runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Set up Python
@ -63,8 +23,7 @@ jobs:
run: ruff check --fix apiserver
lint-admin:
needs: get-changed-files
if: needs.get-changed-files.outputs.admin_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -76,8 +35,7 @@ jobs:
- run: yarn lint --filter=admin
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -89,8 +47,7 @@ jobs:
- run: yarn lint --filter=space
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View file

@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -25,7 +25,7 @@
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
"axios": "^1.7.9",
"axios": "^1.8.3",
"lodash": "^4.17.21",
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",

View file

@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"

View file

@ -268,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
source_data = serializers.SerializerMethodField()
def get_source_data(self, obj):
if (
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return {
"source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email,
"extra": obj.issue.source_data[0].extra,
}
return None
class Meta:
model = IssueActivity

View file

@ -14,7 +14,7 @@ from rest_framework import status
from .. import BaseAPIView
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import IssueActivity, IssueComment, CommentReaction
from plane.db.models import IssueActivity, IssueComment, CommentReaction, IntakeIssue
class IssueActivityEndpoint(BaseAPIView):
@ -57,13 +57,22 @@ class IssueActivityEndpoint(BaseAPIView):
)
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
issue_activities = issue_activities.prefetch_related(
Prefetch(
"issue__issue_intake",
queryset=IntakeIssue.objects.only(
"source_email", "source", "extra"
),
to_attr="source_data",
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
return Response(issue_activities, status=status.HTTP_200_OK)
if request.GET.get("activity_type", None) == "issue-comment":
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
return Response(issue_comments, status=status.HTTP_200_OK)
result_list = sorted(

View file

@ -177,6 +177,7 @@ class ProjectViewSet(BaseViewSet):
"module_view",
"page_view",
"inbox_view",
"guest_view_all_features",
"project_lead",
"created_at",
"updated_at",

View file

@ -117,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
)
def destroy(self, request, slug, pk):
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)

View file

@ -34,6 +34,22 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
def post(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug)
# If the favorite exists return
if request.data.get("entity_identifier"):
user_favorites = UserFavorite.objects.filter(
workspace=workspace,
user_id=request.user.id,
entity_type=request.data.get("entity_type"),
entity_identifier=request.data.get("entity_identifier"),
).first()
# If the favorite exists return
if user_favorites:
serializer = UserFavoriteSerializer(user_favorites)
return Response(serializer.data, status=status.HTTP_200_OK)
# else create a new favorite
serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(

View file

@ -15,34 +15,35 @@ app = Celery("plane")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Executes every day at 12 AM
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-file-asset": {
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=0, minute=0),
},
# Intra day recurring jobs
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"),
},
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
"schedule": crontab(minute="*/5"), # Every 5 minutes
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6", minute=0),
"schedule": crontab(hour="*/6", minute=0), # Every 6 hours
},
# Occurs once every day
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0), # UTC 00:00
},
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=1, minute=0), # UTC 01:00
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=1, minute=30), # UTC 01:30
},
"check-every-day-to-delete-file-asset": {
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=2, minute=0), # UTC 02:00
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=2, minute=30), # UTC 02:30
},
}

View file

@ -1,13 +1,13 @@
{
"name": "live",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
"private": true,
"type": "module",
"scripts": {
"dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"dev": "PORT=3100 concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
"start": "node dist/server.js",
"lint": "eslint src --ext .ts,.tsx",
@ -27,7 +27,7 @@
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
"axios": "^1.7.9",
"axios": "^1.8.3",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
@ -59,7 +59,7 @@
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
"tsup": "^7.2.0",
"tsup": "^8.4.0",
"typescript": "5.3.3"
}
}

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.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@ -28,7 +28,9 @@
},
"resolutions": {
"nanoid": "3.3.8",
"esbuild": "0.25.0"
"esbuild": "0.25.0",
"@babel/helpers": "7.26.10",
"@babel/runtime": "7.26.10"
},
"packageManager": "yarn@1.22.22"
}

View file

@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"main": "./src/index.ts",
"license": "AGPL-3.0"

View file

@ -1,8 +1,5 @@
// icons
import {
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
@ -55,11 +52,11 @@ export const GROUP_CHOICES = {
};
export const PROJECT_AUTOMATION_MONTHS = [
{ i18n_label: "common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 },
{ i18n_label: "workspace_projects.common.months_count", value: 1 },
{ i18n_label: "workspace_projects.common.months_count", value: 3 },
{ i18n_label: "workspace_projects.common.months_count", value: 6 },
{ i18n_label: "workspace_projects.common.months_count", value: 9 },
{ i18n_label: "workspace_projects.common.months_count", value: 12 },
];
export const PROJECT_UNSPLASH_COVERS = [

View file

@ -1,4 +1,4 @@
import { TStaticViewTypes } from "@plane/types";
import { TStaticViewTypes, IWorkspaceSearchResults } from "@plane/types";
import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [
@ -324,3 +324,16 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarN
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
];
export const IS_FAVORITE_MENU_OPEN = "is_favorite_menu_open";
export const WORKSPACE_DEFAULT_SEARCH_RESULT: IWorkspaceSearchResults = {
results: {
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
};

View file

@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.25.2",
"version": "0.25.3",
"description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true,
@ -81,7 +81,7 @@
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"postcss": "^8.4.38",
"tsup": "^7.2.0",
"tsup": "^8.4.0",
"typescript": "5.3.3"
},
"keywords": [

View file

@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"files": [
"library.js",

View file

@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
@ -22,7 +22,7 @@
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"tsup": "^7.2.0",
"tsup": "^8.4.0",
"typescript": "^5.3.3"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally",
"private": true,

View file

@ -7,10 +7,16 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Français", value: "fr" },
{ label: "Español", value: "es" },
{ label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "Русский", value: "ru" },
{ label: "Italian", value: "it" },
{ label: "Čeština", value: "cs" },
{ label: "Slovenčina", value: "sk" },
{ label: "Deutsch", value: "de" },
{ label: "Українська", value: "ua" },
{ label: "Polski", value: "pl" },
{ label: "한국어", value: "ko" },
];
export const STORAGE_KEY = "userLanguage";
export const LANGUAGE_STORAGE_KEY = "userLanguage";

View file

@ -1,8 +1,8 @@
import { useContext } from 'react';
import { useContext } from "react";
// context
import { TranslationContext } from '../context';
import { TranslationContext } from "../context";
// types
import { ILanguageOption, TLanguage } from '../types';
import { ILanguageOption, TLanguage } from "../types";
export type TTranslationStore = {
t: (key: string, params?: Record<string, any>) => string;
@ -23,7 +23,7 @@ export type TTranslationStore = {
export function useTranslation(): TTranslationStore {
const store = useContext(TranslationContext);
if (!store) {
throw new Error('useTranslation must be used within a TranslationProvider');
throw new Error("useTranslation must be used within a TranslationProvider");
}
return {

View file

@ -6,7 +6,7 @@
"home": "Domov",
"your_work": "Vaše práce",
"inbox": "Doručená pošta",
"workspace": "workspace",
"workspace": "Pracovní prostor",
"views": "Pohledy",
"analytics": "Analytika",
"work_items": "Pracovní položky",
@ -1473,7 +1473,8 @@
"max_length": "Název prostoru nesmí přesáhnout 80 znaků"
},
"company_size": {
"required": "Velikost společnosti je povinná"
"required": "Velikost společnosti je povinná",
"select_a_range": "Vyberte velikost organizace"
}
}
},

File diff suppressed because it is too large Load diff

View file

@ -1305,7 +1305,8 @@
"max_length": "Workspace name should not exceed 80 characters"
},
"company_size": {
"required": "Company size is required"
"required": "Company size is required",
"select_a_range": "Select organization size"
}
}
},

View file

@ -1475,7 +1475,8 @@
"max_length": "El nombre del espacio de trabajo no debe exceder los 80 caracteres"
},
"company_size": {
"required": "El tamaño de la empresa es obligatorio"
"required": "El tamaño de la empresa es obligatorio",
"select_a_range": "Seleccionar tamaño de la organización"
}
}
},

View file

@ -1473,7 +1473,8 @@
"max_length": "Le nom de l'espace de travail ne doit pas dépasser 80 caractères"
},
"company_size": {
"required": "La taille de l'entreprise est requise"
"required": "La taille de l'entreprise est requise",
"select_a_range": "Sélectionner la taille de l'organisation"
}
}
},

View file

@ -1471,7 +1471,8 @@
"max_length": "Il nome dello spazio di lavoro non deve superare gli 80 caratteri"
},
"company_size": {
"required": "La dimensione aziendale è obbligatoria"
"required": "La dimensione aziendale è obbligatoria",
"select_a_range": "Seleziona la dimensione dell'organizzazione"
}
}
},

View file

@ -1473,7 +1473,8 @@
"max_length": "ワークスペース名は80文字を超えることはできません"
},
"company_size": {
"required": "会社の規模は必須です"
"required": "会社の規模は必須です",
"select_a_range": "組織の規模を選択"
}
}
},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1473,7 +1473,8 @@
"max_length": "Максимум 80 символов"
},
"company_size": {
"required": "Размер компании обязателен"
"required": "Размер компании обязателен",
"select_a_range": "Выберите размер организации"
}
}
},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1473,7 +1473,8 @@
"max_length": "工作区名称不应超过80个字符"
},
"company_size": {
"required": "公司规模为必填项"
"required": "公司规模为必填项",
"select_a_range": "选择组织规模"
}
}
},

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ import get from "lodash/get";
import merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants";
// core translations imports
import coreEn from "../locales/en/core.json";
// types
@ -48,14 +48,14 @@ export class TranslationStore {
private initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
const savedLocale = localStorage.getItem(LANGUAGE_STORAGE_KEY) as TLanguage;
if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale);
return;
}
const browserLang = this.getBrowserLanguage();
this.setLanguage(browserLang);
// Fallback to default language
this.setLanguage(FALLBACK_LANGUAGE);
}
/** Loads the translations for the current language */
@ -147,12 +147,24 @@ export class TranslationStore {
return import("../locales/ja/translations.json");
case "zh-CN":
return import("../locales/zh-CN/translations.json");
case "zh-TW":
return import("../locales/zh-TW/translations.json");
case "ru":
return import("../locales/ru/translations.json");
case "it":
return import("../locales/it/translations.json");
case "cs":
return import("../locales/cs/translations.json");
case "sk":
return import("../locales/sk/translations.json");
case "de":
return import("../locales/de/translations.json");
case "ua":
return import("../locales/ua/translations.json");
case "pl":
return import("../locales/pl/translations.json");
case "ko":
return import("../locales/ko/translations.json");
default:
throw new Error(`Unsupported language: ${language}`);
}
@ -163,40 +175,6 @@ export class TranslationStore {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
}
/** Checks if a language code is similar to any supported language */
private findSimilarLanguage(lang: string): TLanguage | null {
// Convert to lowercase for case-insensitive comparison
const normalizedLang = lang.toLowerCase();
// Find a supported language that includes or is included in the browser language
const similarLang = this.availableLanguages.find(
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
);
return similarLang ? similarLang.value : null;
}
/** Gets the browser language based on the navigator.language */
private getBrowserLanguage(): TLanguage {
const browserLang = navigator.language;
// Check exact match first
if (this.isValidLanguage(browserLang)) {
return browserLang;
}
// Check base language without region code
const baseLang = browserLang.split("-")[0];
if (this.isValidLanguage(baseLang)) {
return baseLang as TLanguage;
}
// Try to find a similar language
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
return similarLang || FALLBACK_LANGUAGE;
}
/**
* Gets the cache key for the given key and locale
* @param key - the key to get the cache key for
@ -281,7 +259,7 @@ export class TranslationStore {
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng);
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
document.documentElement.lang = lng;
}

View file

@ -1,4 +1,4 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it" | "cs";
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "zh-TW" | "ru" | "it" | "cs" | "sk" | "de" | "ua" | "pl" | "ko";
export interface ILanguageOption {
label: string;

View file

@ -1,6 +1,6 @@
{
"name": "@plane/logger",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "Logger shared across multiple apps internally",
"private": true,

View file

@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"license": "AGPL-3.0",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"main": "./src/index.ts",
@ -10,6 +10,6 @@
},
"dependencies": {
"@plane/constants": "*",
"axios": "^1.7.9"
"axios": "^1.8.3"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally",
"private": true,

View file

@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",

View file

@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"types": "./src/index.d.ts",

View file

@ -66,8 +66,10 @@ export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
results: TInboxIssue[];
};
export type TAnchors = { [key: string]: string };
export type TInboxForm = {
anchor: string;
anchors: TAnchors;
id: string;
is_in_app_enabled: boolean;
is_form_enabled: boolean;

View file

@ -30,6 +30,13 @@ export type TIssueActivity = {
new_identifier: string | undefined;
epoch: number;
issue_comment: string | null;
source_data: {
source: "IN_APP" | "FORM" | "EMAIL";
source_email?: string;
extra: {
username?: string;
};
};
};
export type TIssueActivityMap = {

View file

@ -25,6 +25,7 @@ export interface IPartialProject {
module_view: boolean;
page_view: boolean;
inbox_view: boolean;
guest_view_all_features?: boolean;
project_lead?: IUserLite | string | null;
// Timestamps
created_at?: Date;
@ -46,11 +47,8 @@ export interface IProject extends IPartialProject {
default_state?: string | null;
description?: string;
estimate?: string | null;
guest_view_all_features?: boolean;
anchor?: string | null;
is_favorite?: boolean;
is_issue_type_enabled?: boolean;
is_time_tracking_enabled?: boolean;
members?: string[];
network?: number;
timezone?: string;

View file

@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"files": [

View file

@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.25.2",
"version": "0.25.3",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@ -71,10 +71,7 @@
"postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1",
"storybook": "^8.1.1",
"tsup": "^7.2.0",
"tsup": "^8.4.0",
"typescript": "5.3.3"
},
"resolutions": {
"@types/react": "^18.0.0"
}
}

View file

@ -8,7 +8,12 @@ import { cn } from "../helpers";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => (
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
const currentYear = new Date().getFullYear();
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
const thirtyYearsFromNowFirstDay = new Date(currentYear + 30, 11, 31);
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
@ -63,16 +68,16 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro
<ChevronLeft
className={cn(
"size-4",
{
"rotate-180": props.orientation === "right",
"-rotate-90": props.orientation === "down",
},
{ "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
className
)}
{...props}
/>
),
}}
startMonth={thirtyYearsAgoFirstDay}
endMonth={thirtyYearsFromNowFirstDay}
{...props}
/>
);
);
};

View file

@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "0.25.2",
"version": "0.25.3",
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true,
@ -29,7 +29,7 @@
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"@types/zxcvbn": "^4.4.5",
"tsup": "^7.2.0",
"tsup": "^8.4.0",
"typescript": "^5.3.3"
}
}

View file

@ -30,6 +30,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<link rel="icon" type="image/png" sizes="16x16" href={`${SPACE_BASE_PATH}/favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${SPACE_BASE_PATH}/site.webmanifest.json`} />
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
<meta name="robots" content="noindex, nofollow" />
</head>
<body>
<AppProvider>

View file

@ -31,7 +31,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
})}
{...rest}
containerClassName={containerClassName}
editorClassName="min-h-[100px] max-h-[50vh] border border-gray-100 rounded-md pl-3 pb-3 overflow-y-scroll"
editorClassName="min-h-[100px] max-h-[50vh] border-[0.5px] border-custom-border-200 rounded-md pl-3 py-2 overflow-y-scroll"
/>
);
});

View file

@ -1,11 +1,12 @@
"use client";
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// types
import { TIssuePriorities } from "@plane/types";
import { Tooltip } from "@plane/ui";
import { PriorityIcon, Tooltip } from "@plane/ui";
// constants
import { getIssuePriorityFilters } from "@plane/utils";
import { cn, getIssuePriorityFilters } from "@plane/utils";
export const IssueBlockPriority = ({
priority,
@ -18,14 +19,47 @@ export const IssueBlockPriority = ({
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
const priorityClasses = {
urgent: "bg-red-600/10 text-red-600 border-red-600 px-1",
high: "bg-orange-500/20 text-orange-950 border-orange-500",
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
none: "hover:bg-custom-background-80 border-custom-border-300",
};
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<div className="flex items-center relative w-full h-full">
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
</div>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
priorityClasses[priority ?? "none"],
{
// compact the icons if text is hidden
"px-0.5": !shouldShowName,
// highlight the whole button if text is hidden and priority is urgent
"bg-red-600/10 border-red-600": priority === "urgent" && shouldShowName,
}
)}
>
{priority ? (
<PriorityIcon
priority={priority}
size={12}
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": !shouldShowName,
// centre align the icons if text is hidden
"translate-x-[0.0625rem]": !shouldShowName && priority === "high",
"translate-x-0.5": !shouldShowName && priority === "medium",
"translate-x-1": !shouldShowName && priority === "low",
// highlight the icon if priority is urgent
})}
/>
) : (
<SignalHigh className="size-3" />
)}
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
</div>
</Tooltip>

View file

@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@ -26,7 +26,7 @@
"@plane/ui": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.54.0",
"axios": "^1.7.9",
"axios": "^1.8.3",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"dompurify": "^3.0.11",

2
space/public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -38,7 +38,7 @@ const ProjectCyclesPage = observer(() => {
// derived values
const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - ${t("cycles.label", { count: 2 })}` : undefined;
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined;
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const hasMemberLevelPermission = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],

View file

@ -16,14 +16,16 @@ import { generateWorkItemLink } from "@/helpers/issue.helper";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues";
export const commandGroups: {
export type TCommandGroups = {
[key: string]: {
icon: JSX.Element | null;
itemName: (item: any) => React.ReactNode;
path: (item: any, projectId: string | undefined) => string;
title: string;
};
} = {
};
export const commandGroups: TCommandGroups = {
cycle: {
icon: <ContrastIcon className="h-3 w-3" />,
itemName: (cycle: IWorkspaceDefaultSearchResult) => (

View file

@ -0,0 +1,3 @@
export * from "./actions";
export * from "./modals";
export * from "./helpers";

View file

@ -1 +0,0 @@
export * from "./values";

View file

@ -1 +0,0 @@
export * from "./update";

View file

@ -1,12 +0,0 @@
import { TIssueServiceType } from "@plane/types";
export type TIssueAdditionalPropertyValuesUpdateProps = {
issueId: string;
issueTypeId: string;
projectId: string;
workspaceSlug: string;
isDisabled: boolean;
issueServiceType?: TIssueServiceType;
};
export const IssueAdditionalPropertyValuesUpdate: React.FC<TIssueAdditionalPropertyValuesUpdateProps> = () => <></>;

View file

@ -0,0 +1,13 @@
import React, { FC } from "react";
// plane imports
export type TWorkItemAdditionalSidebarProperties = {
workItemId: string;
workItemTypeId: string | null;
projectId: string;
workspaceSlug: string;
isEditable: boolean;
isPeekView?: boolean;
};
export const WorkItemAdditionalSidebarProperties: FC<TWorkItemAdditionalSidebarProperties> = () => <></>;

View file

@ -3,3 +3,4 @@ export * from "./issue-properties-activity";
export * from "./issue-type-switcher";
export * from "./issue-type-activity";
export * from "./parent-select-root";
export * from "./issue-creator";

View file

@ -0,0 +1,36 @@
import { FC } from "react";
import Link from "next/link";
// hooks
import { useIssueDetail } from "@/hooks/store";
type TIssueUser = {
activityId: string;
customUserName?: string;
};
export const IssueCreatorDisplay: FC<TIssueUser> = (props) => {
const { activityId, customUserName } = props;
// hooks
const {
activity: { getActivityById },
} = useIssueDetail();
const activity = getActivityById(activityId);
if (!activity) return <></>;
return (
<>
{customUserName ? (
<span className="text-custom-text-100 font-medium">{customUserName || "Plane"}</span>
) : (
<Link
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
className="hover:underline text-custom-text-100 font-medium"
>
{activity.actor_detail?.display_name}
</Link>
)}
</>
);
};

View file

@ -5,7 +5,7 @@ import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { Eye, EyeClosed } from "lucide-react";
import { Pin, PinOff } from "lucide-react";
// plane imports
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
@ -197,16 +197,16 @@ export const ExtendedSidebarItem: FC<TExtendedSidebarItemProps> = observer((prop
</div>
)}
{isPinned ? (
<Tooltip tooltipContent="Hide tab">
<Eye
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-300 outline-none"
<Tooltip tooltipContent="Unpin">
<PinOff
className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
onClick={() => unPinNavigationItem(workspaceSlug.toString(), item.key)}
/>
</Tooltip>
) : (
<Tooltip tooltipContent="Show tab">
<EyeClosed
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-400 outline-none"
<Tooltip tooltipContent="Pin">
<Pin
className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
onClick={() => pinNavigationItem(workspaceSlug.toString(), item.key)}
/>
</Tooltip>

View file

@ -11,12 +11,7 @@ export type TProperties = {
icon: ReactNode;
isPro: boolean;
isEnabled: boolean;
renderChildren?: (
currentProjectDetails: IProject,
isAdmin: boolean,
handleSubmit: (featureKey: string, featureProperty: string) => Promise<void>,
workspaceSlug: string
) => ReactNode;
renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode;
};
export type TFeatureList = {
[key: string]: TProperties;

View file

@ -1,5 +1,7 @@
// types
import { IIssueDisplayProperties } from "@plane/types";
// lib
import { store } from "@/lib/store-context";
export type TShouldRenderDisplayProperty = {
workspaceSlug: string;
@ -16,3 +18,13 @@ export const shouldRenderDisplayProperty = (props: TShouldRenderDisplayProperty)
return true;
}
};
export const shouldRenderColumn = (key: keyof IIssueDisplayProperties): boolean => {
const isEstimateEnabled: boolean = store.projectRoot.project.currentProjectDetails?.estimate !== null;
switch (key) {
case "estimate":
return isEstimateEnabled;
default:
return true;
}
};

View file

@ -0,0 +1,4 @@
import { TIssue } from "@plane/types";
import { getIssueIds } from "@/store/issue/helpers/base-issues-utils";
export const workItemSortWithOrderByExtended = (array: TIssue[], key?: string) => getIssueIds(array);

View file

@ -0,0 +1 @@
export * from "@/store/inbox/project-inbox.store";

View file

@ -97,7 +97,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.i18n_label} value={month.value}>
<span className="text-sm">{t(month.i18n_label, { month: month.value })}</span>
<span className="text-sm">{t(month.i18n_label, { months: month.value })}</span>
</CustomSelect.Option>
))}
@ -106,7 +106,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
className="flex w-full select-none items-center rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
{t("customize_time_range")}
{t("common.customize_time_range")}
</button>
</>
</CustomSelect>

View file

@ -124,7 +124,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.i18n_label} value={month.value}>
{t(month.i18n_label, { month: month.value })}
{t(month.i18n_label, { months: month.value })}
</CustomSelect.Option>
))}
<button
@ -132,7 +132,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
{t("customize_time_range")}
{t("common.customize_time_range")}
</button>
</>
</CustomSelect>

View file

@ -2,12 +2,12 @@
import { Command } from "cmdk";
import { useParams } from "next/navigation";
// types
// plane imports
import { IWorkspaceSearchResults } from "@plane/types";
// helpers
import { commandGroups } from "@/components/command-palette";
// hooks
import { useAppRouter } from "@/hooks/use-app-router";
// plane web imports
import { commandGroups } from "@/plane-web/components/command-palette";
type Props = {
closePalette: () => void;
@ -25,9 +25,9 @@ export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
return (
<>
{Object.keys(results.results).map((key) => {
// TODO: add type for results
const section = (results.results as any)[key];
const currentSection = commandGroups[key];
if (!currentSection) return null;
if (section.length > 0) {
return (

View file

@ -8,7 +8,7 @@ import useSWR from "swr";
import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { IWorkspaceSearchResults } from "@plane/types";
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
@ -58,9 +58,7 @@ export const CommandModal: React.FC = observer(() => {
const [isLoading, setIsLoading] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<IWorkspaceSearchResults>({
results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
});
const [results, setResults] = useState<IWorkspaceSearchResults>(WORKSPACE_DEFAULT_SEARCH_RESULT);
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const [pages, setPages] = useState<string[]>([]);
const [searchInIssue, setSearchInIssue] = useState(false);
@ -151,9 +149,7 @@ export const CommandModal: React.FC = observer(() => {
setIsSearching(false);
});
} else {
setResults({
results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
});
setResults(WORKSPACE_DEFAULT_SEARCH_RESULT);
setIsLoading(false);
setIsSearching(false);
}

View file

@ -2,4 +2,3 @@ export * from "./actions";
export * from "./shortcuts-modal";
export * from "./command-modal";
export * from "./command-palette";
export * from "./helpers";

View file

@ -6,7 +6,14 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { Eye, Users } from "lucide-react";
// types
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import {
CYCLE_FAVORITED,
CYCLE_UNFAVORITED,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types";
// ui
@ -15,8 +22,6 @@ import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, s
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
// constants
// helpers
import { getDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
@ -59,6 +64,12 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
const { getUserDetails } = useMember();
// form
@ -91,6 +102,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
captureEvent(CYCLE_FAVORITED, {
cycle_id: cycleId,
element: "List layout",

View file

@ -21,13 +21,27 @@ export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props)
const activity = getActivityById(activityId);
if (!activity) return <></>;
const source = activity.source_data?.source;
return (
<IssueActivityBlockComponent
activityId={activityId}
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
ends={ends}
>
<>{activity.verb === "created" ? " created the work item." : " deleted a work item."}</>
<>
{activity.verb === "created" ? (
source && source !== "IN_APP" ? (
<span>
created the work item via <span className="font-medium">{source.toLowerCase()}</span>.
</span>
) : (
<span> created the work item.</span>
)
) : (
<span> deleted a work item.</span>
)}
</>
</IssueActivityBlockComponent>
);
});

View file

@ -9,6 +9,7 @@ import { useIssueDetail } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// ui
// components
import { IssueCreatorDisplay } from "@/plane-web/components/issues";
import { IssueUser } from "../";
// helpers
@ -41,7 +42,11 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
{icon ? icon : <Network className="w-3.5 h-3.5" />}
</div>
<div className="w-full truncate text-custom-text-200">
{!activity?.field && activity?.verb === "created" ? (
<IssueCreatorDisplay activityId={activityId} customUserName={customUserName} />
) : (
<IssueUser activityId={activityId} customUserName={customUserName} />
)}
<span> {children} </span>
<span>
<Tooltip

View file

@ -371,7 +371,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/>
</div>
<div
className="fixed right-0 z-[5] h-full w-full min-w-[300px] overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
className="fixed right-0 z-[5] h-full w-full min-w-[300px] border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
style={issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<IssueDetailsSidebar

View file

@ -24,9 +24,9 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
// hooks
import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useMember } from "@/hooks/store";
// plane web components
import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values";
import { IssueParentSelectRoot, IssueWorklogProperty } from "@/plane-web/components/issues";
// components
import { WorkItemAdditionalSidebarProperties } from "@/plane-web/components/issues/issue-details/additional-properties";
import type { TIssueOperations } from "./root";
type Props = {
@ -293,15 +293,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
disabled={!isEditable}
/>
{issue.type_id && (
<IssueAdditionalPropertyValuesUpdate
issueId={issueId}
issueTypeId={issue.type_id}
<WorkItemAdditionalSidebarProperties
workItemId={issue.id}
workItemTypeId={issue.type_id}
projectId={projectId}
workspaceSlug={workspaceSlug}
isDisabled={!isEditable}
isEditable={isEditable}
isPeekView
/>
)}
</div>
</div>
</div>

View file

@ -7,6 +7,7 @@ import { IIssueDisplayProperties, TIssue } from "@plane/types";
import { useEventTracker } from "@/hooks/store";
// components
import { SPREADSHEET_COLUMNS } from "@/plane-web/components/issues/issue-layouts/utils";
import { shouldRenderColumn } from "@/plane-web/helpers/issue-filter.helper";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
// utils
@ -20,13 +21,13 @@ type Props = {
};
export const IssueColumn = observer((props: Props) => {
const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props;
const { displayProperties, issueDetail, disableUserActions, property, updateIssue } = props;
// router
const pathname = usePathname();
const tableCellRef = useRef<HTMLTableCellElement | null>(null);
const { captureIssueEvent } = useEventTracker();
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
const shouldRenderProperty = shouldRenderColumn(property);
const Column = SPREADSHEET_COLUMNS[property];

View file

@ -3,6 +3,7 @@ import { useRef } from "react";
import { observer } from "mobx-react";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
//components
import { shouldRenderColumn } from "@/plane-web/helpers/issue-filter.helper";
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import { HeaderColumn } from "./columns/header-column";
@ -15,19 +16,12 @@ interface Props {
isEpic?: boolean;
}
export const SpreadsheetHeaderColumn = observer((props: Props) => {
const {
displayProperties,
displayFilters,
property,
isEstimateEnabled,
handleDisplayFilterUpdate,
isEpic = false,
} = props;
const { displayProperties, displayFilters, property, handleDisplayFilterUpdate, isEpic = false } = props;
//hooks
const tableHeaderCellRef = useRef<HTMLTableCellElement | null>(null);
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
const shouldRenderProperty = shouldRenderColumn(property);
return (
<WithDisplayPropertiesHOC

View file

@ -23,8 +23,8 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
// plane web components
import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values";
import { IssueParentSelectRoot, IssueWorklogProperty } from "@/plane-web/components/issues";
import { WorkItemAdditionalSidebarProperties } from "@/plane-web/components/issues/issue-details/additional-properties";
interface IPeekOverviewProperties {
workspaceSlug: string;
@ -291,15 +291,14 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
disabled={disabled}
/>
{issue.type_id && (
<IssueAdditionalPropertyValuesUpdate
issueId={issueId}
issueTypeId={issue.type_id}
<WorkItemAdditionalSidebarProperties
workItemId={issue.id}
workItemTypeId={issue.type_id}
projectId={projectId}
workspaceSlug={workspaceSlug}
isDisabled={disabled}
isEditable={!disabled}
isPeekView
/>
)}
</div>
</div>
);

View file

@ -13,7 +13,9 @@ import {
MODULE_UNFAVORITED,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { IModule } from "@plane/types";
import {
Card,
@ -30,7 +32,6 @@ import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { ModuleQuickActions } from "@/components/modules";
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
// constants
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { generateQueryParams } from "@/helpers/router.helper";
@ -59,6 +60,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const { getUserDetails } = useMember();
const { captureEvent } = useEventTracker();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = allowPermissions(
@ -76,6 +80,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_FAVORITED, {
module_id: moduleId,
element: "Grid layout",

View file

@ -12,7 +12,9 @@ import {
MODULE_UNFAVORITED,
EUserPermissions,
EUserPermissionsLevel,
IS_FAVORITE_MENU_OPEN,
} from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { IModule } from "@plane/types";
// ui
@ -45,6 +47,8 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
const { t } = useTranslation();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
// derived values
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);
@ -63,6 +67,8 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
() => {
// open favorites menu if closed
if (!storedValue) toggleFavoriteMenu(true);
captureEvent(MODULE_FAVORITED, {
module_id: moduleId,
element: "Grid layout",

View file

@ -90,20 +90,17 @@ export const ModulesListView: React.FC = observer(() => {
return (
<ContentWrapper variant={ERowVariant.HUGGING}>
<div className="size-full flex justify-between">
{displayFilters?.layout === "list" && (
<div className="flex h-full w-full justify-between">
<ListLayout>
{filteredModuleIds.map((moduleId) => (
<ModuleListItem key={moduleId} moduleId={moduleId} />
))}
</ListLayout>
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
</div>
)}
{displayFilters?.layout === "board" && (
<Row className="flex h-full w-full justify-between py-page-y">
<div
className={`grid h-full w-full grid-cols-1 gap-6 overflow-y-auto ${
<Row
className={`size-full py-page-y grid grid-cols-1 gap-6 overflow-y-auto ${
peekModule
? "lg:grid-cols-1 xl:grid-cols-2 3xl:grid-cols-3"
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
@ -112,11 +109,17 @@ export const ModulesListView: React.FC = observer(() => {
{filteredModuleIds.map((moduleId) => (
<ModuleCardItem key={moduleId} moduleId={moduleId} />
))}
</div>
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
</Row>
)}
{displayFilters?.layout === "gantt" && <ModulesListGanttChartView />}
{displayFilters?.layout === "gantt" && (
<div className="size-full overflow-hidden">
<ModulesListGanttChartView />
</div>
)}
<div className="flex-shrink-0">
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
</div>
</div>
</ContentWrapper>
);
});

View file

@ -1,8 +1,12 @@
"use client";
import { observer } from "mobx-react";
// constants
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
// editor
import { EditorRefApi } from "@plane/editor";
// plane hooks
import { useLocalStorage } from "@plane/hooks";
// ui
import { ArchiveIcon, FavoriteStar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
// components
@ -37,6 +41,11 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
} = page;
// use online status
const { isOnline } = useOnlineStatus();
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
// favorite handler
const handleFavorite = () => {
if (is_favorite) {
@ -48,13 +57,14 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
})
);
} else {
addToFavorites().then(() =>
addToFavorites().then(() => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page added to favorites.",
})
);
});
});
}
};

View file

@ -6,7 +6,8 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Trash2, UserPlus } from "lucide-react";
// types
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import type { IProject } from "@plane/types";
// ui
import {
@ -68,6 +69,11 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
const hasMemberRole = project.member_role === EUserPermissions.MEMBER;
// archive
const isArchived = !!project.archived_at;
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
const handleAddToFavorites = () => {
if (!workspaceSlug) return;
@ -78,6 +84,10 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
success: {
title: "Success!",
message: () => "Project added to favorites.",
actionItems: () => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
return <></>;
},
},
error: {
title: "Error!",

View file

@ -84,7 +84,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
<h4 className="text-sm font-medium leading-5">{t(featureItem.key)}</h4>
{featureItem.isPro && (
<Tooltip tooltipContent="Pro feature" position="top">
<UpgradeBadge />
<UpgradeBadge className="rounded" />
</Tooltip>
)}
</div>
@ -103,8 +103,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
</div>
<div className="pl-14">
{currentProjectDetails?.[featureItem.property as keyof IProject] &&
featureItem.renderChildren &&
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit, workspaceSlug)}
featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
</div>
</div>
);

View file

@ -3,13 +3,13 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Earth, Lock } from "lucide-react";
// types
import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { EViewAccess, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { IProjectView } from "@plane/types";
// ui
import { Tooltip, FavoriteStar } from "@plane/ui";
// components
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
// constants
// helpers
import { calculateTotalFilters } from "@/helpers/filter.helper";
import { getPublishViewLink } from "@/helpers/project-views.helpers";
@ -37,6 +37,12 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
const { addViewToFavorites, removeViewFromFavorites } = useProjectView();
const { getUserDetails } = useMember();
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
// derived values
const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@ -50,10 +56,11 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
const publishLink = getPublishViewLink(view?.anchor);
// handlers
const handleAddToFavorites = () => {
const handleAddToFavorites = async () => {
if (!workspaceSlug || !projectId) return;
addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
await addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
if (!isFavoriteOpen) toggleFavoriteMenu(true);
};
const handleRemoveFromFavorites = () => {

View file

@ -0,0 +1,109 @@
import { FC } from "react";
import { TNotification } from "@plane/types";
// components
import { LiteTextReadOnlyEditor } from "@/components/editor";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
export const NotificationContent: FC<{
notification: TNotification;
workspaceId: string;
workspaceSlug: string;
projectId: string;
renderCommentBox?: boolean;
}> = ({ notification, workspaceId, workspaceSlug, projectId, renderCommentBox = false }) => {
const { data, triggered_by_details: triggeredBy } = notification;
const notificationField = data?.issue_activity.field;
const newValue = data?.issue_activity.new_value;
const oldValue = data?.issue_activity.old_value;
const verb = data?.issue_activity.verb;
const renderTriggerName = () => (
<span className="text-custom-text-100 font-medium">
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
</span>
);
const renderAction = () => {
if (!notificationField) return "";
if (notificationField === "duplicate")
return verb === "created"
? "marked that this work item is a duplicate of"
: "marked that this work item is not a duplicate";
if (notificationField === "assignees") {
return newValue !== "" ? "added assignee" : "removed assignee";
}
if (notificationField === "start_date") {
return newValue !== "" ? "set start date" : "removed the start date";
}
if (notificationField === "target_date") {
return newValue !== "" ? "set due date" : "removed the due date";
}
if (notificationField === "labels") {
return newValue !== "" ? "added label" : "removed label";
}
if (notificationField === "parent") {
return newValue !== "" ? "added parent" : "removed parent";
}
if (notificationField === "relates_to") return "marked that this work item is related to";
if (notificationField === "comment") return "commented";
if (notificationField === "archived_at") {
return newValue === "restore" ? "restored the work item" : "archived the work item";
}
if (notificationField === "None") return null;
const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : "";
return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`;
};
const renderValue = () => {
if (notificationField === "None") return "the work item and assigned it to you.";
if (notificationField === "comment") return renderCommentBox ? null : sanitizeCommentForNotification(newValue);
if (notificationField === "target_date" || notificationField === "start_date") return renderFormattedDate(newValue);
if (notificationField === "attachment") return "the work item";
if (notificationField === "description") return stripAndTruncateHTML(newValue || "", 55);
if (notificationField === "archived_at") return null;
if (notificationField === "assignees") return newValue !== "" ? newValue : oldValue;
if (notificationField === "labels") return newValue !== "" ? newValue : oldValue;
if (notificationField === "parent") return newValue !== "" ? newValue : oldValue;
return newValue;
};
const shouldShowConnector = ![
"comment",
"archived_at",
"None",
"assignees",
"labels",
"start_date",
"target_date",
"parent",
].includes(notificationField || "");
return (
<>
{renderTriggerName()}
<span className="text-custom-text-300">{renderAction()} </span>
{verb !== "deleted" && (
<>
{shouldShowConnector && <span className="text-custom-text-300">to </span>}
<span className="text-custom-text-100 font-medium">{renderValue()}</span>
{notificationField === "comment" && renderCommentBox && (
<div className="scale-75 origin-left">
<LiteTextReadOnlyEditor
id=""
initialValue={newValue ?? ""}
workspaceId={workspaceId}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</div>
)}
{"."}
</>
)}
</>
);
};

View file

@ -1,2 +1,3 @@
export * from "./item";
export * from "./options";
export * from "./content";

View file

@ -10,10 +10,9 @@ import { NotificationOption } from "@/components/workspace-notifications";
import { cn } from "@/helpers/common.helper";
import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
// hooks
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store";
import { useIssueDetail, useNotification, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { NotificationContent } from "./content";
type TNotificationItem = {
workspaceSlug: string;
@ -26,6 +25,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
const { getWorkspaceBySlug } = useWorkspace();
// states
const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
@ -33,6 +33,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
// derived values
const projectId = notification?.project || undefined;
const issueId = notification?.data?.issue?.id || undefined;
const workspace = getWorkspaceBySlug(workspaceSlug);
const notificationField = notification?.data?.issue_activity.field || undefined;
const notificationTriggeredBy = notification.triggered_by_details || undefined;
@ -57,7 +58,8 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
}
};
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField || !workspace?.id || !projectId)
return <></>;
return (
<Row
@ -88,56 +90,12 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
<div className="w-full space-y-1 -mt-2">
<div className="relative flex items-center gap-3 h-8">
<div className="w-full overflow-hidden whitespace-normal break-all truncate line-clamp-1 text-sm text-custom-text-100">
{!notification.message ? (
<>
<span className="font-semibold">
{notificationTriggeredBy?.is_bot
? notificationTriggeredBy?.first_name
: notificationTriggeredBy?.display_name}{" "}
</span>
{!["comment", "archived_at"].includes(notificationField) && notification?.data?.issue_activity.verb}{" "}
{notificationField === "comment"
? "commented"
: notificationField === "archived_at"
? notification?.data?.issue_activity.new_value === "restore"
? "restored the work item"
: "archived the work item"
: notificationField === "None"
? null
: replaceUnderscoreIfSnakeCase(notificationField)}{" "}
{notification?.data?.issue_activity.verb !== "deleted" && (
<>
{!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
<span className="font-semibold">
{" "}
{notificationField !== "None" ? (
notificationField !== "comment" ? (
notificationField === "target_date" ? (
renderFormattedDate(notification?.data?.issue_activity.new_value)
) : notificationField === "attachment" ? (
"the work item"
) : notificationField === "description" ? (
stripAndTruncateHTML(notification?.data?.issue_activity.new_value || "", 55)
) : notificationField === "archived_at" ? null : (
notification?.data?.issue_activity.new_value
)
) : (
<span>
{sanitizeCommentForNotification(
notification?.data?.issue_activity.new_value ?? undefined
)}
</span>
)
) : (
"the work item and assigned it to you."
)}
</span>
</>
)}
</>
) : (
<span className="semi-bold">{notification.message}</span>
)}
<NotificationContent
notification={notification}
workspaceId={workspace.id}
workspaceSlug={workspaceSlug}
projectId={projectId}
/>
</div>
<NotificationOption
workspaceSlug={workspaceSlug}

View file

@ -13,6 +13,7 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { ChevronRight, FolderPlus } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
// ui
import { IFavorite } from "@plane/types";
@ -54,7 +55,7 @@ export const SidebarFavoritesMenu = observer(() => {
const { isMobile } = usePlatformOS();
// local storage
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>("is_favorite_menu_open", false);
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
// derived values
const isFavoriteMenuOpen = !!storedValue;
// refs

View file

@ -1,7 +1,7 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "@/lib/store-context";
import { IProjectInboxStore } from "@/store/inbox/project-inbox.store";
import { IProjectInboxStore } from "@/plane-web/store/project-inbox.store";
export const useProjectInbox = (): IProjectInboxStore => {
const context = useContext(StoreContext);

View file

@ -1,4 +1,6 @@
import { useMemo } from "react";
// plane constants
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
// plane editor
import { EditorRefApi } from "@plane/editor";
// plane types
@ -11,6 +13,8 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions";
// store types
import { TPageInstance } from "@/store/pages/base-page";
// local storage
import useLocalStorage from "./use-local-storage";
export type TPageOperations = {
toggleLock: () => void;
@ -46,6 +50,11 @@ export const usePageOperations = (
} = page;
// collaborative actions
const { executeCollaborativeAction } = useCollaborativePageActions(props);
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isfavoriteMenuOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
// page operations
const pageOperations: TPageOperations = useMemo(() => {
const pageLink = getRedirectionLink();
@ -141,13 +150,14 @@ export const usePageOperations = (
})
);
} else {
addToFavorites().then(() =>
addToFavorites().then(() => {
if (!isfavoriteMenuOpen) toggleFavoriteMenu(true);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Page added to favorites.",
})
);
});
});
}
},
toggleLock: async () => {

View file

@ -2,7 +2,7 @@ import * as Comlink from "comlink";
import set from "lodash/set";
// plane
import { EIssueGroupBYServerToProperty } from "@plane/constants";
import { TIssue } from "@plane/types";
import { TIssue, TIssueParams } from "@plane/types";
// lib
import { rootStore } from "@/lib/store-context";
// services
@ -15,6 +15,7 @@ import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
import { loadWorkSpaceData } from "./utils/load-workspace";
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
import { runQuery } from "./utils/query-executor";
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
import { createTables } from "./utils/tables";
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
@ -269,7 +270,12 @@ export class Storage {
return issue.updated_at;
};
getIssues = async (workspaceSlug: string, projectId: string, queries: any, config: any) => {
getIssues = async (
workspaceSlug: string,
projectId: string,
queries: Partial<Record<TIssueParams, string | boolean>> | undefined,
config: any
) => {
log("#### Queries", queries);
const currentProjectStatus = this.getStatus(projectId);
@ -294,11 +300,12 @@ export class Storage {
}
}
const { cursor, group_by, sub_group_by } = queries;
const sanitizedQueries = sanitizeWorkItemQueries(workspaceSlug, projectId, queries);
const { cursor, group_by, sub_group_by } = sanitizedQueries || {};
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, sanitizedQueries);
log("#### Query", query);
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, sanitizedQueries);
const start = performance.now();
let issuesRaw: any[] = [];
let count: any[];
@ -313,7 +320,7 @@ export class Storage {
const { total_count } = count[0];
const [pageSize, page, offset] = cursor.split(":");
const [pageSize, page, offset] = cursor && typeof cursor === "string" ? cursor.split(":") : [];
const groupByProperty: string =
EIssueGroupBYServerToProperty[group_by as keyof typeof EIssueGroupBYServerToProperty];

View file

@ -0,0 +1,41 @@
// plane constants
import { EUserPermissions } from "@plane/constants";
import { TIssueParams } from "@plane/types";
// root store
import { rootStore } from "@/lib/store-context";
export const sanitizeWorkItemQueries = (
workspaceSlug: string,
projectId: string,
queries: Partial<Record<TIssueParams, string | boolean>> | undefined
): Partial<Record<TIssueParams, string | boolean>> | undefined => {
// Get current project details and user id and role for the project
const currentProject = rootStore.projectRoot.project.getProjectById(projectId);
const currentUserId = rootStore.user.data?.id;
const currentUserRole = rootStore.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug,
projectId
);
// Only apply this restriction for guests when guest_view_all_features is disabled
if (
currentUserId &&
currentUserRole === EUserPermissions.GUEST &&
currentProject?.guest_view_all_features === false
) {
// Sanitize the created_by filter if it doesn't exist or if it exists and includes the current user id
const existingCreatedByFilter = queries?.created_by;
const shouldApplyFilter =
!existingCreatedByFilter ||
(typeof existingCreatedByFilter === "string" && existingCreatedByFilter.includes(currentUserId));
if (shouldApplyFilter) {
queries = {
...queries,
created_by: currentUserId,
};
}
}
return queries;
};

View file

@ -1,6 +1,6 @@
// types
import { TInboxIssue } from "@plane/constants";
import type { TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
import type { TIssue, TInboxIssueWithPagination } from "@plane/types";
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
// helpers
@ -76,28 +76,4 @@ export class InboxIssueService extends APIService {
throw error?.response?.data;
});
}
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async updatePublishForm(workspaceSlug: string, projectId: string, data: Partial<TInboxForm>): Promise<TInboxForm> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}

View file

@ -1,6 +1,7 @@
import { EIssueServiceType } from "@plane/constants";
// types
import {
TIssueParams,
type IIssueDisplayProperties,
type TBulkOperationsPayload,
type TIssue,
@ -75,7 +76,12 @@ export class IssueService extends APIService {
});
}
async getIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise<TIssuesResponse> {
async getIssues(
workspaceSlug: string,
projectId: string,
queries?: Partial<Record<TIssueParams, string | boolean>>,
config = {}
): Promise<TIssuesResponse> {
if (getIssuesShouldFallbackToServer(queries) || this.serviceType !== EIssueServiceType.ISSUES) {
return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config);
}

View file

@ -111,9 +111,10 @@ export class FavoriteStore implements IFavoriteStore {
* @returns Promise<IFavorite>
*/
addFavorite = async (workspaceSlug: string, data: Partial<IFavorite>) => {
const id = uuidv4();
data = { ...data, parent: null, is_folder: data.entity_type === "folder" };
if (data.entity_identifier && this.entityMap[data.entity_identifier]) return this.entityMap[data.entity_identifier];
const id = uuidv4();
try {
// optimistic addition
runInAction(() => {
@ -271,6 +272,7 @@ export class FavoriteStore implements IFavoriteStore {
* @returns Promise<void>
*/
deleteFavorite = async (workspaceSlug: string, favoriteId: string) => {
if (!this.favoriteMap[favoriteId]) return;
const parent = this.favoriteMap[favoriteId].parent;
const children = this.groupedFavorites[favoriteId].children;
const entity_identifier = this.favoriteMap[favoriteId].entity_identifier;

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