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_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }} gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }} 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_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }} dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
@ -123,46 +117,7 @@ jobs:
name: Checkout Files name: Checkout Files
uses: actions/checkout@v4 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: 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 name: Build-Push Admin Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
@ -185,7 +140,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_web: 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 name: Build-Push Web Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
@ -208,7 +162,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_space: 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 name: Build-Push Space Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
@ -231,7 +184,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_live: 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 name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
@ -254,7 +206,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_apiserver: 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 name: Build-Push API Server Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]
@ -277,7 +228,6 @@ jobs:
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }} buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
branch_build_push_proxy: 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 name: Build-Push Proxy Docker Image
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [branch_build_setup] needs: [branch_build_setup]

View file

@ -6,49 +6,9 @@ on:
types: ["opened", "synchronize", "ready_for_review"] types: ["opened", "synchronize", "ready_for_review"]
jobs: jobs:
get-changed-files: lint-apiserver:
if: github.event.pull_request.draft == false if: github.event.pull_request.draft == false
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
@ -63,8 +23,7 @@ jobs:
run: ruff check --fix apiserver run: ruff check --fix apiserver
lint-admin: lint-admin:
needs: get-changed-files if: github.event.pull_request.draft == false
if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -76,8 +35,7 @@ jobs:
- run: yarn lint --filter=admin - run: yarn lint --filter=admin
lint-space: lint-space:
needs: get-changed-files if: github.event.pull_request.draft == false
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -89,8 +47,7 @@ jobs:
- run: yarn lint --filter=space - run: yarn lint --filter=space
lint-web: lint-web:
needs: get-changed-files if: github.event.pull_request.draft == false
if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

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

View file

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

View file

@ -268,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue") issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") 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: class Meta:
model = IssueActivity model = IssueActivity

View file

@ -14,7 +14,7 @@ from rest_framework import status
from .. import BaseAPIView from .. import BaseAPIView
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE 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): 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": 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) return Response(issue_activities, status=status.HTTP_200_OK)
if request.GET.get("activity_type", None) == "issue-comment": 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) return Response(issue_comments, status=status.HTTP_200_OK)
result_list = sorted( result_list = sorted(

View file

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

View file

@ -117,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @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): def destroy(self, request, slug, pk):
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug) workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)

View file

@ -34,6 +34,22 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
def post(self, request, slug): def post(self, request, slug):
try: try:
workspace = Workspace.objects.get(slug=slug) 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) serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save( serializer.save(

View file

@ -15,34 +15,35 @@ app = Celery("plane")
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = { app.conf.beat_schedule = {
# Executes every day at 12 AM # Intra day recurring jobs
"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),
},
"check-every-five-minutes-to-send-email-notifications": { "check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification", "task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"), "schedule": crontab(minute="*/5"), # Every 5 minutes
},
"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),
}, },
"run-every-6-hours-for-instance-trace": { "run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces", "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", "name": "live",
"version": "0.25.2", "version": "0.25.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor", "description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts", "main": "./src/server.ts",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "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\"", "build": "babel src --out-dir dist --extensions \".ts,.js\"",
"start": "node dist/server.js", "start": "node dist/server.js",
"lint": "eslint src --ext .ts,.tsx", "lint": "eslint src --ext .ts,.tsx",
@ -27,7 +27,7 @@
"@sentry/profiling-node": "^8.28.0", "@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "2.10.4", "@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0", "@tiptap/html": "2.11.0",
"axios": "^1.7.9", "axios": "^1.8.3",
"compression": "^1.7.4", "compression": "^1.7.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@ -59,7 +59,7 @@
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"nodemon": "^3.1.7", "nodemon": "^3.1.7",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsup": "^7.2.0", "tsup": "^8.4.0",
"typescript": "5.3.3" "typescript": "5.3.3"
} }
} }

View file

@ -2,7 +2,7 @@
"name": "plane", "name": "plane",
"description": "Open-source project management that unlocks customer value", "description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git", "repository": "https://github.com/makeplane/plane.git",
"version": "0.25.2", "version": "0.25.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"workspaces": [ "workspaces": [
@ -28,7 +28,9 @@
}, },
"resolutions": { "resolutions": {
"nanoid": "3.3.8", "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" "packageManager": "yarn@1.22.22"
} }

View file

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

View file

@ -1,8 +1,5 @@
// icons // icons
import { import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
TProjectAppliedDisplayFilterKeys,
TProjectOrderByOptions,
} from "@plane/types";
export type TNetworkChoiceIconKey = "Lock" | "Globe2"; export type TNetworkChoiceIconKey = "Lock" | "Globe2";
@ -55,11 +52,11 @@ export const GROUP_CHOICES = {
}; };
export const PROJECT_AUTOMATION_MONTHS = [ export const PROJECT_AUTOMATION_MONTHS = [
{ i18n_label: "common.months_count", value: 1 }, { i18n_label: "workspace_projects.common.months_count", value: 1 },
{ i18n_label: "common.months_count", value: 3 }, { i18n_label: "workspace_projects.common.months_count", value: 3 },
{ i18n_label: "common.months_count", value: 6 }, { i18n_label: "workspace_projects.common.months_count", value: 6 },
{ i18n_label: "common.months_count", value: 9 }, { i18n_label: "workspace_projects.common.months_count", value: 9 },
{ i18n_label: "common.months_count", value: 12 }, { i18n_label: "workspace_projects.common.months_count", value: 12 },
]; ];
export const PROJECT_UNSPLASH_COVERS = [ 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"; import { EUserWorkspaceRoles } from "./user";
export const ORGANIZATION_SIZE = [ 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["inbox"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"], 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", "name": "@plane/editor",
"version": "0.25.2", "version": "0.25.3",
"description": "Core Editor that powers Plane", "description": "Core Editor that powers Plane",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
@ -81,7 +81,7 @@
"@types/react": "^18.3.11", "@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"tsup": "^7.2.0", "tsup": "^8.4.0",
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"keywords": [ "keywords": [

View file

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

View file

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

View file

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

View file

@ -7,10 +7,16 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Français", value: "fr" }, { label: "Français", value: "fr" },
{ label: "Español", value: "es" }, { label: "Español", value: "es" },
{ label: "日本語", value: "ja" }, { label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" }, { label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "Русский", value: "ru" }, { label: "Русский", value: "ru" },
{ label: "Italian", value: "it" }, { label: "Italian", value: "it" },
{ label: "Čeština", value: "cs" }, { 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 // context
import { TranslationContext } from '../context'; import { TranslationContext } from "../context";
// types // types
import { ILanguageOption, TLanguage } from '../types'; import { ILanguageOption, TLanguage } from "../types";
export type TTranslationStore = { export type TTranslationStore = {
t: (key: string, params?: Record<string, any>) => string; t: (key: string, params?: Record<string, any>) => string;
@ -23,7 +23,7 @@ export type TTranslationStore = {
export function useTranslation(): TTranslationStore { export function useTranslation(): TTranslationStore {
const store = useContext(TranslationContext); const store = useContext(TranslationContext);
if (!store) { if (!store) {
throw new Error('useTranslation must be used within a TranslationProvider'); throw new Error("useTranslation must be used within a TranslationProvider");
} }
return { return {

View file

@ -6,7 +6,7 @@
"home": "Domov", "home": "Domov",
"your_work": "Vaše práce", "your_work": "Vaše práce",
"inbox": "Doručená pošta", "inbox": "Doručená pošta",
"workspace": "workspace", "workspace": "Pracovní prostor",
"views": "Pohledy", "views": "Pohledy",
"analytics": "Analytika", "analytics": "Analytika",
"work_items": "Pracovní položky", "work_items": "Pracovní položky",
@ -1473,7 +1473,8 @@
"max_length": "Název prostoru nesmí přesáhnout 80 znaků" "max_length": "Název prostoru nesmí přesáhnout 80 znaků"
}, },
"company_size": { "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" "max_length": "Workspace name should not exceed 80 characters"
}, },
"company_size": { "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" "max_length": "El nombre del espacio de trabajo no debe exceder los 80 caracteres"
}, },
"company_size": { "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" "max_length": "Le nom de l'espace de travail ne doit pas dépasser 80 caractères"
}, },
"company_size": { "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" "max_length": "Il nome dello spazio di lavoro non deve superare gli 80 caratteri"
}, },
"company_size": { "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文字を超えることはできません" "max_length": "ワークスペース名は80文字を超えることはできません"
}, },
"company_size": { "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 символов" "max_length": "Максимум 80 символов"
}, },
"company_size": { "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个字符" "max_length": "工作区名称不应超过80个字符"
}, },
"company_size": { "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 merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx"; import { makeAutoObservable, runInAction } from "mobx";
// constants // constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants"; import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants";
// core translations imports // core translations imports
import coreEn from "../locales/en/core.json"; import coreEn from "../locales/en/core.json";
// types // types
@ -48,14 +48,14 @@ export class TranslationStore {
private initializeLanguage() { private initializeLanguage() {
if (typeof window === "undefined") return; 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)) { if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale); this.setLanguage(savedLocale);
return; return;
} }
const browserLang = this.getBrowserLanguage(); // Fallback to default language
this.setLanguage(browserLang); this.setLanguage(FALLBACK_LANGUAGE);
} }
/** Loads the translations for the current language */ /** Loads the translations for the current language */
@ -147,12 +147,24 @@ export class TranslationStore {
return import("../locales/ja/translations.json"); return import("../locales/ja/translations.json");
case "zh-CN": case "zh-CN":
return import("../locales/zh-CN/translations.json"); return import("../locales/zh-CN/translations.json");
case "zh-TW":
return import("../locales/zh-TW/translations.json");
case "ru": case "ru":
return import("../locales/ru/translations.json"); return import("../locales/ru/translations.json");
case "it": case "it":
return import("../locales/it/translations.json"); return import("../locales/it/translations.json");
case "cs": case "cs":
return import("../locales/cs/translations.json"); 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: default:
throw new Error(`Unsupported language: ${language}`); throw new Error(`Unsupported language: ${language}`);
} }
@ -163,40 +175,6 @@ export class TranslationStore {
return lang !== null && this.availableLanguages.some((l) => l.value === lang); 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 * Gets the cache key for the given key and locale
* @param key - the key to get the cache key for * @param key - the key to get the cache key for
@ -281,7 +259,7 @@ export class TranslationStore {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng); localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
document.documentElement.lang = 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 { export interface ILanguageOption {
label: string; label: string;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
"name": "@plane/ui", "name": "@plane/ui",
"description": "UI components shared across multiple apps internally", "description": "UI components shared across multiple apps internally",
"private": true, "private": true,
"version": "0.25.2", "version": "0.25.3",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -71,10 +71,7 @@
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.0",
"postcss-nested": "^6.0.1", "postcss-nested": "^6.0.1",
"storybook": "^8.1.1", "storybook": "^8.1.1",
"tsup": "^7.2.0", "tsup": "^8.4.0",
"typescript": "5.3.3" "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 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 <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn("p-3", className)} className={cn("p-3", className)}
@ -63,16 +68,16 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro
<ChevronLeft <ChevronLeft
className={cn( className={cn(
"size-4", "size-4",
{ { "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
"rotate-180": props.orientation === "right",
"-rotate-90": props.orientation === "down",
},
className className
)} )}
{...props} {...props}
/> />
), ),
}} }}
startMonth={thirtyYearsAgoFirstDay}
endMonth={thirtyYearsFromNowFirstDay}
{...props} {...props}
/> />
); );
};

View file

@ -1,6 +1,6 @@
{ {
"name": "@plane/utils", "name": "@plane/utils",
"version": "0.25.2", "version": "0.25.3",
"description": "Helper functions shared across multiple apps internally", "description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
@ -29,7 +29,7 @@
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/react": "^18.3.11", "@types/react": "^18.3.11",
"@types/zxcvbn": "^4.4.5", "@types/zxcvbn": "^4.4.5",
"tsup": "^7.2.0", "tsup": "^8.4.0",
"typescript": "^5.3.3" "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="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="manifest" href={`${SPACE_BASE_PATH}/site.webmanifest.json`} />
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} /> <link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
<meta name="robots" content="noindex, nofollow" />
</head> </head>
<body> <body>
<AppProvider> <AppProvider>

View file

@ -31,7 +31,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
})} })}
{...rest} {...rest}
containerClassName={containerClassName} 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"; "use client";
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { TIssuePriorities } from "@plane/types"; import { TIssuePriorities } from "@plane/types";
import { Tooltip } from "@plane/ui"; import { PriorityIcon, Tooltip } from "@plane/ui";
// constants // constants
import { getIssuePriorityFilters } from "@plane/utils"; import { cn, getIssuePriorityFilters } from "@plane/utils";
export const IssueBlockPriority = ({ export const IssueBlockPriority = ({
priority, priority,
@ -18,14 +19,47 @@ export const IssueBlockPriority = ({
const { t } = useTranslation(); const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null; 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 <></>; if (priority_detail === null) return <></>;
return ( return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}> <Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<div className="flex items-center relative w-full h-full"> <div
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}> className={cn(
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span> "h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
</div> 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>} {shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
</div> </div>
</Tooltip> </Tooltip>

View file

@ -1,6 +1,6 @@
{ {
"name": "space", "name": "space",
"version": "0.25.2", "version": "0.25.3",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -26,7 +26,7 @@
"@plane/ui": "*", "@plane/ui": "*",
"@plane/services": "*", "@plane/services": "*",
"@sentry/nextjs": "^8.54.0", "@sentry/nextjs": "^8.54.0",
"axios": "^1.7.9", "axios": "^1.8.3",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.0.11", "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 // derived values
const totalCycles = currentProjectCycleIds?.length ?? 0; const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = projectId ? getProjectById(projectId?.toString()) : undefined; 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 hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const hasMemberLevelPermission = allowPermissions( const hasMemberLevelPermission = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],

View file

@ -16,14 +16,16 @@ import { generateWorkItemLink } from "@/helpers/issue.helper";
// plane web components // plane web components
import { IssueIdentifier } from "@/plane-web/components/issues"; import { IssueIdentifier } from "@/plane-web/components/issues";
export const commandGroups: { export type TCommandGroups = {
[key: string]: { [key: string]: {
icon: JSX.Element | null; icon: JSX.Element | null;
itemName: (item: any) => React.ReactNode; itemName: (item: any) => React.ReactNode;
path: (item: any, projectId: string | undefined) => string; path: (item: any, projectId: string | undefined) => string;
title: string; title: string;
}; };
} = { };
export const commandGroups: TCommandGroups = {
cycle: { cycle: {
icon: <ContrastIcon className="h-3 w-3" />, icon: <ContrastIcon className="h-3 w-3" />,
itemName: (cycle: IWorkspaceDefaultSearchResult) => ( 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-switcher";
export * from "./issue-type-activity"; export * from "./issue-type-activity";
export * from "./parent-select-root"; 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 { observer } from "mobx-react";
import Link from "next/link"; import Link from "next/link";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { Eye, EyeClosed } from "lucide-react"; import { Pin, PinOff } from "lucide-react";
// plane imports // plane imports
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants"; import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@ -197,16 +197,16 @@ export const ExtendedSidebarItem: FC<TExtendedSidebarItemProps> = observer((prop
</div> </div>
)} )}
{isPinned ? ( {isPinned ? (
<Tooltip tooltipContent="Hide tab"> <Tooltip tooltipContent="Unpin">
<Eye <PinOff
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-300 outline-none" className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
onClick={() => unPinNavigationItem(workspaceSlug.toString(), item.key)} onClick={() => unPinNavigationItem(workspaceSlug.toString(), item.key)}
/> />
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip tooltipContent="Show tab"> <Tooltip tooltipContent="Pin">
<EyeClosed <Pin
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-400 outline-none" className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
onClick={() => pinNavigationItem(workspaceSlug.toString(), item.key)} onClick={() => pinNavigationItem(workspaceSlug.toString(), item.key)}
/> />
</Tooltip> </Tooltip>

View file

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

View file

@ -1,5 +1,7 @@
// types // types
import { IIssueDisplayProperties } from "@plane/types"; import { IIssueDisplayProperties } from "@plane/types";
// lib
import { store } from "@/lib/store-context";
export type TShouldRenderDisplayProperty = { export type TShouldRenderDisplayProperty = {
workspaceSlug: string; workspaceSlug: string;
@ -16,3 +18,13 @@ export const shouldRenderDisplayProperty = (props: TShouldRenderDisplayProperty)
return true; 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) => ( {PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.i18n_label} value={month.value}> <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> </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" 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)} onClick={() => setmonthModal(true)}
> >
{t("customize_time_range")} {t("common.customize_time_range")}
</button> </button>
</> </>
</CustomSelect> </CustomSelect>

View file

@ -124,7 +124,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
<> <>
{PROJECT_AUTOMATION_MONTHS.map((month) => ( {PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.i18n_label} value={month.value}> <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> </CustomSelect.Option>
))} ))}
<button <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" 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)} onClick={() => setmonthModal(true)}
> >
{t("customize_time_range")} {t("common.customize_time_range")}
</button> </button>
</> </>
</CustomSelect> </CustomSelect>

View file

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

View file

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

View file

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

View file

@ -21,13 +21,27 @@ export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props)
const activity = getActivityById(activityId); const activity = getActivityById(activityId);
if (!activity) return <></>; if (!activity) return <></>;
const source = activity.source_data?.source;
return ( return (
<IssueActivityBlockComponent <IssueActivityBlockComponent
activityId={activityId} activityId={activityId}
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />} icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
ends={ends} 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> </IssueActivityBlockComponent>
); );
}); });

View file

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

View file

@ -371,7 +371,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
/> />
</div> </div>
<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` } : {}} style={issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
> >
<IssueDetailsSidebar <IssueDetailsSidebar

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,8 @@ import Link from "next/link";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Trash2, UserPlus } from "lucide-react"; import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Trash2, UserPlus } from "lucide-react";
// types // 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"; import type { IProject } from "@plane/types";
// ui // ui
import { import {
@ -68,6 +69,11 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
const hasMemberRole = project.member_role === EUserPermissions.MEMBER; const hasMemberRole = project.member_role === EUserPermissions.MEMBER;
// archive // archive
const isArchived = !!project.archived_at; const isArchived = !!project.archived_at;
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
const handleAddToFavorites = () => { const handleAddToFavorites = () => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
@ -78,6 +84,10 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
success: { success: {
title: "Success!", title: "Success!",
message: () => "Project added to favorites.", message: () => "Project added to favorites.",
actionItems: () => {
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
return <></>;
},
}, },
error: { error: {
title: "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> <h4 className="text-sm font-medium leading-5">{t(featureItem.key)}</h4>
{featureItem.isPro && ( {featureItem.isPro && (
<Tooltip tooltipContent="Pro feature" position="top"> <Tooltip tooltipContent="Pro feature" position="top">
<UpgradeBadge /> <UpgradeBadge className="rounded" />
</Tooltip> </Tooltip>
)} )}
</div> </div>
@ -103,8 +103,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
</div> </div>
<div className="pl-14"> <div className="pl-14">
{currentProjectDetails?.[featureItem.property as keyof IProject] && {currentProjectDetails?.[featureItem.property as keyof IProject] &&
featureItem.renderChildren && featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit, workspaceSlug)}
</div> </div>
</div> </div>
); );

View file

@ -3,13 +3,13 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Earth, Lock } from "lucide-react"; import { Earth, Lock } from "lucide-react";
// types // 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"; import { IProjectView } from "@plane/types";
// ui // ui
import { Tooltip, FavoriteStar } from "@plane/ui"; import { Tooltip, FavoriteStar } from "@plane/ui";
// components // components
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views"; import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
// constants
// helpers // helpers
import { calculateTotalFilters } from "@/helpers/filter.helper"; import { calculateTotalFilters } from "@/helpers/filter.helper";
import { getPublishViewLink } from "@/helpers/project-views.helpers"; import { getPublishViewLink } from "@/helpers/project-views.helpers";
@ -37,6 +37,12 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
const { addViewToFavorites, removeViewFromFavorites } = useProjectView(); const { addViewToFavorites, removeViewFromFavorites } = useProjectView();
const { getUserDetails } = useMember(); const { getUserDetails } = useMember();
// local storage
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteOpen } = useLocalStorage<boolean>(
IS_FAVORITE_MENU_OPEN,
false
);
// derived values // derived values
const isEditingAllowed = allowPermissions( const isEditingAllowed = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER], [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@ -50,10 +56,11 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
const publishLink = getPublishViewLink(view?.anchor); const publishLink = getPublishViewLink(view?.anchor);
// handlers // handlers
const handleAddToFavorites = () => { const handleAddToFavorites = async () => {
if (!workspaceSlug || !projectId) return; 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 = () => { 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 "./item";
export * from "./options"; 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 { cn } from "@/helpers/common.helper";
import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper"; import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper"; import { getFileURL } from "@/helpers/file.helper";
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
// hooks // hooks
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store"; import { useIssueDetail, useNotification, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
import { NotificationContent } from "./content";
type TNotificationItem = { type TNotificationItem = {
workspaceSlug: string; workspaceSlug: string;
@ -26,6 +25,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications(); const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId); const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
const { getWorkspaceBySlug } = useWorkspace();
// states // states
const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false); const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
const [customSnoozeModal, setCustomSnoozeModal] = useState(false); const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
@ -33,6 +33,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
// derived values // derived values
const projectId = notification?.project || undefined; const projectId = notification?.project || undefined;
const issueId = notification?.data?.issue?.id || undefined; const issueId = notification?.data?.issue?.id || undefined;
const workspace = getWorkspaceBySlug(workspaceSlug);
const notificationField = notification?.data?.issue_activity.field || undefined; const notificationField = notification?.data?.issue_activity.field || undefined;
const notificationTriggeredBy = notification.triggered_by_details || 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 ( return (
<Row <Row
@ -88,56 +90,12 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
<div className="w-full space-y-1 -mt-2"> <div className="w-full space-y-1 -mt-2">
<div className="relative flex items-center gap-3 h-8"> <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"> <div className="w-full overflow-hidden whitespace-normal break-all truncate line-clamp-1 text-sm text-custom-text-100">
{!notification.message ? ( <NotificationContent
<> notification={notification}
<span className="font-semibold"> workspaceId={workspace.id}
{notificationTriggeredBy?.is_bot workspaceSlug={workspaceSlug}
? notificationTriggeredBy?.first_name projectId={projectId}
: 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>
)}
</div> </div>
<NotificationOption <NotificationOption
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}

View file

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

View file

@ -1,7 +1,7 @@
import { useContext } from "react"; import { useContext } from "react";
// mobx store // mobx store
import { StoreContext } from "@/lib/store-context"; 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 => { export const useProjectInbox = (): IProjectInboxStore => {
const context = useContext(StoreContext); const context = useContext(StoreContext);

View file

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

View file

@ -2,7 +2,7 @@ import * as Comlink from "comlink";
import set from "lodash/set"; import set from "lodash/set";
// plane // plane
import { EIssueGroupBYServerToProperty } from "@plane/constants"; import { EIssueGroupBYServerToProperty } from "@plane/constants";
import { TIssue } from "@plane/types"; import { TIssue, TIssueParams } from "@plane/types";
// lib // lib
import { rootStore } from "@/lib/store-context"; import { rootStore } from "@/lib/store-context";
// services // services
@ -15,6 +15,7 @@ import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
import { loadWorkSpaceData } from "./utils/load-workspace"; import { loadWorkSpaceData } from "./utils/load-workspace";
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor"; import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
import { runQuery } from "./utils/query-executor"; import { runQuery } from "./utils/query-executor";
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
import { createTables } from "./utils/tables"; import { createTables } from "./utils/tables";
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils"; import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
@ -269,7 +270,12 @@ export class Storage {
return issue.updated_at; 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); log("#### Queries", queries);
const currentProjectStatus = this.getStatus(projectId); 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); log("#### Query", query);
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries); const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, sanitizedQueries);
const start = performance.now(); const start = performance.now();
let issuesRaw: any[] = []; let issuesRaw: any[] = [];
let count: any[]; let count: any[];
@ -313,7 +320,7 @@ export class Storage {
const { total_count } = count[0]; const { total_count } = count[0];
const [pageSize, page, offset] = cursor.split(":"); const [pageSize, page, offset] = cursor && typeof cursor === "string" ? cursor.split(":") : [];
const groupByProperty: string = const groupByProperty: string =
EIssueGroupBYServerToProperty[group_by as keyof typeof EIssueGroupBYServerToProperty]; 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 // types
import { TInboxIssue } from "@plane/constants"; 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 { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service"; import { APIService } from "@/services/api.service";
// helpers // helpers
@ -76,28 +76,4 @@ export class InboxIssueService extends APIService {
throw error?.response?.data; 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"; import { EIssueServiceType } from "@plane/constants";
// types // types
import { import {
TIssueParams,
type IIssueDisplayProperties, type IIssueDisplayProperties,
type TBulkOperationsPayload, type TBulkOperationsPayload,
type TIssue, 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) { if (getIssuesShouldFallbackToServer(queries) || this.serviceType !== EIssueServiceType.ISSUES) {
return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config); return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config);
} }

View file

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

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