release: v0.25.3 #6788
This commit is contained in:
commit
45e25ce18b
114 changed files with 15068 additions and 766 deletions
50
.github/workflows/build-branch.yml
vendored
50
.github/workflows/build-branch.yml
vendored
|
|
@ -47,12 +47,6 @@ jobs:
|
|||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
|
||||
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
|
||||
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
|
||||
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
|
||||
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
|
||||
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
|
||||
build_live: ${{ steps.changed_files.outputs.live_any_changed }}
|
||||
|
||||
dh_img_web: ${{ steps.set_env_variables.outputs.DH_IMG_WEB }}
|
||||
dh_img_space: ${{ steps.set_env_variables.outputs.DH_IMG_SPACE }}
|
||||
|
|
@ -123,46 +117,7 @@ jobs:
|
|||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
uses: tj-actions/changed-files@v42
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
proxy:
|
||||
- nginx/**
|
||||
admin:
|
||||
- admin/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
space:
|
||||
- space/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
web:
|
||||
- web/**
|
||||
- packages/**
|
||||
- "package.json"
|
||||
- "yarn.lock"
|
||||
- "tsconfig.json"
|
||||
- "turbo.json"
|
||||
live:
|
||||
- live/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
|
||||
branch_build_push_admin:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
@ -185,7 +140,6 @@ jobs:
|
|||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_web:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Web Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
@ -208,7 +162,6 @@ jobs:
|
|||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_space:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Space Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
@ -231,7 +184,6 @@ jobs:
|
|||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_live:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
@ -254,7 +206,6 @@ jobs:
|
|||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_apiserver:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push API Server Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
@ -277,7 +228,6 @@ jobs:
|
|||
buildx-endpoint: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||
|
||||
branch_build_push_proxy:
|
||||
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
name: Build-Push Proxy Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
|
|
|
|||
51
.github/workflows/build-test-pull-request.yml
vendored
51
.github/workflows/build-test-pull-request.yml
vendored
|
|
@ -6,49 +6,9 @@ on:
|
|||
types: ["opened", "synchronize", "ready_for_review"]
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
lint-apiserver:
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
|
||||
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
|
||||
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
|
||||
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
admin:
|
||||
- admin/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
space:
|
||||
- space/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
web:
|
||||
- web/**
|
||||
- packages/**
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
- 'tsconfig.json'
|
||||
- 'turbo.json'
|
||||
|
||||
lint-apiserver:
|
||||
needs: get-changed-files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
|
|
@ -63,8 +23,7 @@ jobs:
|
|||
run: ruff check --fix apiserver
|
||||
|
||||
lint-admin:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.admin_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -76,8 +35,7 @@ jobs:
|
|||
- run: yarn lint --filter=admin
|
||||
|
||||
lint-space:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.space_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -89,8 +47,7 @@ jobs:
|
|||
- run: yarn lint --filter=space
|
||||
|
||||
lint-web:
|
||||
needs: get-changed-files
|
||||
if: needs.get-changed-files.outputs.web_changed == 'true'
|
||||
if: github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
|
|
|||
|
|
@ -268,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
|
|||
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
source_data = serializers.SerializerMethodField()
|
||||
|
||||
def get_source_data(self, obj):
|
||||
if (
|
||||
hasattr(obj, "issue")
|
||||
and hasattr(obj.issue, "source_data")
|
||||
and obj.issue.source_data
|
||||
):
|
||||
return {
|
||||
"source": obj.issue.source_data[0].source,
|
||||
"source_email": obj.issue.source_data[0].source_email,
|
||||
"extra": obj.issue.source_data[0].extra,
|
||||
}
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from rest_framework import status
|
|||
from .. import BaseAPIView
|
||||
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
|
||||
from plane.db.models import IssueActivity, IssueComment, CommentReaction
|
||||
from plane.db.models import IssueActivity, IssueComment, CommentReaction, IntakeIssue
|
||||
|
||||
|
||||
class IssueActivityEndpoint(BaseAPIView):
|
||||
|
|
@ -57,13 +57,22 @@ class IssueActivityEndpoint(BaseAPIView):
|
|||
)
|
||||
)
|
||||
)
|
||||
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
|
||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||
|
||||
if request.GET.get("activity_type", None) == "issue-property":
|
||||
issue_activities = issue_activities.prefetch_related(
|
||||
Prefetch(
|
||||
"issue__issue_intake",
|
||||
queryset=IntakeIssue.objects.only(
|
||||
"source_email", "source", "extra"
|
||||
),
|
||||
to_attr="source_data",
|
||||
)
|
||||
)
|
||||
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
|
||||
return Response(issue_activities, status=status.HTTP_200_OK)
|
||||
|
||||
if request.GET.get("activity_type", None) == "issue-comment":
|
||||
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||
return Response(issue_comments, status=status.HTTP_200_OK)
|
||||
|
||||
result_list = sorted(
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ class ProjectViewSet(BaseViewSet):
|
|||
"module_view",
|
||||
"page_view",
|
||||
"inbox_view",
|
||||
"guest_view_all_features",
|
||||
"project_lead",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
|
||||
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
|
||||
)
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,22 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
|||
def post(self, request, slug):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# If the favorite exists return
|
||||
if request.data.get("entity_identifier"):
|
||||
user_favorites = UserFavorite.objects.filter(
|
||||
workspace=workspace,
|
||||
user_id=request.user.id,
|
||||
entity_type=request.data.get("entity_type"),
|
||||
entity_identifier=request.data.get("entity_identifier"),
|
||||
).first()
|
||||
|
||||
# If the favorite exists return
|
||||
if user_favorites:
|
||||
serializer = UserFavoriteSerializer(user_favorites)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# else create a new favorite
|
||||
serializer = UserFavoriteSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
|
|
|
|||
|
|
@ -15,34 +15,35 @@ app = Celery("plane")
|
|||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
# Executes every day at 12 AM
|
||||
"check-every-day-to-archive-and-close": {
|
||||
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete_exporter_history": {
|
||||
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete-file-asset": {
|
||||
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
# Intra day recurring jobs
|
||||
"check-every-five-minutes-to-send-email-notifications": {
|
||||
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
},
|
||||
"check-every-day-to-delete-hard-delete": {
|
||||
"task": "plane.bgtasks.deletion_task.hard_delete",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete-api-logs": {
|
||||
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
"schedule": crontab(minute="*/5"), # Every 5 minutes
|
||||
},
|
||||
"run-every-6-hours-for-instance-trace": {
|
||||
"task": "plane.license.bgtasks.tracer.instance_traces",
|
||||
"schedule": crontab(hour="*/6", minute=0),
|
||||
"schedule": crontab(hour="*/6", minute=0), # Every 6 hours
|
||||
},
|
||||
# Occurs once every day
|
||||
"check-every-day-to-delete-hard-delete": {
|
||||
"task": "plane.bgtasks.deletion_task.hard_delete",
|
||||
"schedule": crontab(hour=0, minute=0), # UTC 00:00
|
||||
},
|
||||
"check-every-day-to-archive-and-close": {
|
||||
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
||||
"schedule": crontab(hour=1, minute=0), # UTC 01:00
|
||||
},
|
||||
"check-every-day-to-delete_exporter_history": {
|
||||
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
|
||||
"schedule": crontab(hour=1, minute=30), # UTC 01:30
|
||||
},
|
||||
"check-every-day-to-delete-file-asset": {
|
||||
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
|
||||
"schedule": crontab(hour=2, minute=0), # UTC 02:00
|
||||
},
|
||||
"check-every-day-to-delete-api-logs": {
|
||||
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
|
||||
"schedule": crontab(hour=2, minute=30), # UTC 02:30
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "live",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
|
||||
"dev": "PORT=3100 concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
|
||||
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
|
||||
"start": "node dist/server.js",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"@sentry/profiling-node": "^8.28.0",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/html": "2.11.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
|
@ -28,7 +28,9 @@
|
|||
},
|
||||
"resolutions": {
|
||||
"nanoid": "3.3.8",
|
||||
"esbuild": "0.25.0"
|
||||
"esbuild": "0.25.0",
|
||||
"@babel/helpers": "7.26.10",
|
||||
"@babel/runtime": "7.26.10"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"license": "AGPL-3.0"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
// icons
|
||||
import {
|
||||
TProjectAppliedDisplayFilterKeys,
|
||||
TProjectOrderByOptions,
|
||||
} from "@plane/types";
|
||||
import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types";
|
||||
|
||||
export type TNetworkChoiceIconKey = "Lock" | "Globe2";
|
||||
|
||||
|
|
@ -55,11 +52,11 @@ export const GROUP_CHOICES = {
|
|||
};
|
||||
|
||||
export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ i18n_label: "common.months_count", value: 1 },
|
||||
{ i18n_label: "common.months_count", value: 3 },
|
||||
{ i18n_label: "common.months_count", value: 6 },
|
||||
{ i18n_label: "common.months_count", value: 9 },
|
||||
{ i18n_label: "common.months_count", value: 12 },
|
||||
{ i18n_label: "workspace_projects.common.months_count", value: 1 },
|
||||
{ i18n_label: "workspace_projects.common.months_count", value: 3 },
|
||||
{ i18n_label: "workspace_projects.common.months_count", value: 6 },
|
||||
{ i18n_label: "workspace_projects.common.months_count", value: 9 },
|
||||
{ i18n_label: "workspace_projects.common.months_count", value: 12 },
|
||||
];
|
||||
|
||||
export const PROJECT_UNSPLASH_COVERS = [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TStaticViewTypes } from "@plane/types";
|
||||
import { TStaticViewTypes, IWorkspaceSearchResults } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "./user";
|
||||
|
||||
export const ORGANIZATION_SIZE = [
|
||||
|
|
@ -324,3 +324,16 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarN
|
|||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
|
||||
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
|
||||
];
|
||||
|
||||
export const IS_FAVORITE_MENU_OPEN = "is_favorite_menu_open";
|
||||
export const WORKSPACE_DEFAULT_SEARCH_RESULT: IWorkspaceSearchResults = {
|
||||
results: {
|
||||
workspace: [],
|
||||
project: [],
|
||||
issue: [],
|
||||
cycle: [],
|
||||
module: [],
|
||||
issue_view: [],
|
||||
page: [],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"postcss": "^8.4.38",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@plane/eslint-config",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
"library.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/hooks",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "React hooks that are shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"@plane/eslint-config": "*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/i18n",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "I18n shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,16 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
|
|||
{ label: "Français", value: "fr" },
|
||||
{ label: "Español", value: "es" },
|
||||
{ label: "日本語", value: "ja" },
|
||||
{ label: "中文", value: "zh-CN" },
|
||||
{ label: "简体中文", value: "zh-CN" },
|
||||
{ label: "繁體中文", value: "zh-TW" },
|
||||
{ label: "Русский", value: "ru" },
|
||||
{ label: "Italian", value: "it" },
|
||||
{ label: "Čeština", value: "cs" },
|
||||
{ label: "Slovenčina", value: "sk" },
|
||||
{ label: "Deutsch", value: "de" },
|
||||
{ label: "Українська", value: "ua" },
|
||||
{ label: "Polski", value: "pl" },
|
||||
{ label: "한국어", value: "ko" },
|
||||
];
|
||||
|
||||
export const STORAGE_KEY = "userLanguage";
|
||||
export const LANGUAGE_STORAGE_KEY = "userLanguage";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useContext } from 'react';
|
||||
import { useContext } from "react";
|
||||
// context
|
||||
import { TranslationContext } from '../context';
|
||||
import { TranslationContext } from "../context";
|
||||
// types
|
||||
import { ILanguageOption, TLanguage } from '../types';
|
||||
import { ILanguageOption, TLanguage } from "../types";
|
||||
|
||||
export type TTranslationStore = {
|
||||
t: (key: string, params?: Record<string, any>) => string;
|
||||
|
|
@ -23,7 +23,7 @@ export type TTranslationStore = {
|
|||
export function useTranslation(): TTranslationStore {
|
||||
const store = useContext(TranslationContext);
|
||||
if (!store) {
|
||||
throw new Error('useTranslation must be used within a TranslationProvider');
|
||||
throw new Error("useTranslation must be used within a TranslationProvider");
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"home": "Domov",
|
||||
"your_work": "Vaše práce",
|
||||
"inbox": "Doručená pošta",
|
||||
"workspace": "workspace",
|
||||
"workspace": "Pracovní prostor",
|
||||
"views": "Pohledy",
|
||||
"analytics": "Analytika",
|
||||
"work_items": "Pracovní položky",
|
||||
|
|
@ -1473,7 +1473,8 @@
|
|||
"max_length": "Název prostoru nesmí přesáhnout 80 znaků"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "Velikost společnosti je povinná"
|
||||
"required": "Velikost společnosti je povinná",
|
||||
"select_a_range": "Vyberte velikost organizace"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
2324
packages/i18n/src/locales/de/translations.json
Normal file
2324
packages/i18n/src/locales/de/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1305,7 +1305,8 @@
|
|||
"max_length": "Workspace name should not exceed 80 characters"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "Company size is required"
|
||||
"required": "Company size is required",
|
||||
"select_a_range": "Select organization size"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1475,7 +1475,8 @@
|
|||
"max_length": "El nombre del espacio de trabajo no debe exceder los 80 caracteres"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "El tamaño de la empresa es obligatorio"
|
||||
"required": "El tamaño de la empresa es obligatorio",
|
||||
"select_a_range": "Seleccionar tamaño de la organización"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1473,7 +1473,8 @@
|
|||
"max_length": "Le nom de l'espace de travail ne doit pas dépasser 80 caractères"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "La taille de l'entreprise est requise"
|
||||
"required": "La taille de l'entreprise est requise",
|
||||
"select_a_range": "Sélectionner la taille de l'organisation"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1471,7 +1471,8 @@
|
|||
"max_length": "Il nome dello spazio di lavoro non deve superare gli 80 caratteri"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "La dimensione aziendale è obbligatoria"
|
||||
"required": "La dimensione aziendale è obbligatoria",
|
||||
"select_a_range": "Seleziona la dimensione dell'organizzazione"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1473,7 +1473,8 @@
|
|||
"max_length": "ワークスペース名は80文字を超えることはできません"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "会社の規模は必須です"
|
||||
"required": "会社の規模は必須です",
|
||||
"select_a_range": "組織の規模を選択"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
2371
packages/i18n/src/locales/ko/translations.json
Normal file
2371
packages/i18n/src/locales/ko/translations.json
Normal file
File diff suppressed because it is too large
Load diff
2324
packages/i18n/src/locales/pl/translations.json
Normal file
2324
packages/i18n/src/locales/pl/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1473,7 +1473,8 @@
|
|||
"max_length": "Максимум 80 символов"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "Размер компании обязателен"
|
||||
"required": "Размер компании обязателен",
|
||||
"select_a_range": "Выберите размер организации"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
2368
packages/i18n/src/locales/sk/translations.json
Normal file
2368
packages/i18n/src/locales/sk/translations.json
Normal file
File diff suppressed because it is too large
Load diff
2324
packages/i18n/src/locales/ua/translations.json
Normal file
2324
packages/i18n/src/locales/ua/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1473,7 +1473,8 @@
|
|||
"max_length": "工作区名称不应超过80个字符"
|
||||
},
|
||||
"company_size": {
|
||||
"required": "公司规模为必填项"
|
||||
"required": "公司规模为必填项",
|
||||
"select_a_range": "选择组织规模"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
2371
packages/i18n/src/locales/zh-TW/translations.json
Normal file
2371
packages/i18n/src/locales/zh-TW/translations.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ import get from "lodash/get";
|
|||
import merge from "lodash/merge";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
// constants
|
||||
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
|
||||
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants";
|
||||
// core translations imports
|
||||
import coreEn from "../locales/en/core.json";
|
||||
// types
|
||||
|
|
@ -48,14 +48,14 @@ export class TranslationStore {
|
|||
private initializeLanguage() {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
|
||||
const savedLocale = localStorage.getItem(LANGUAGE_STORAGE_KEY) as TLanguage;
|
||||
if (this.isValidLanguage(savedLocale)) {
|
||||
this.setLanguage(savedLocale);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserLang = this.getBrowserLanguage();
|
||||
this.setLanguage(browserLang);
|
||||
// Fallback to default language
|
||||
this.setLanguage(FALLBACK_LANGUAGE);
|
||||
}
|
||||
|
||||
/** Loads the translations for the current language */
|
||||
|
|
@ -147,12 +147,24 @@ export class TranslationStore {
|
|||
return import("../locales/ja/translations.json");
|
||||
case "zh-CN":
|
||||
return import("../locales/zh-CN/translations.json");
|
||||
case "zh-TW":
|
||||
return import("../locales/zh-TW/translations.json");
|
||||
case "ru":
|
||||
return import("../locales/ru/translations.json");
|
||||
case "it":
|
||||
return import("../locales/it/translations.json");
|
||||
case "cs":
|
||||
return import("../locales/cs/translations.json");
|
||||
case "sk":
|
||||
return import("../locales/sk/translations.json");
|
||||
case "de":
|
||||
return import("../locales/de/translations.json");
|
||||
case "ua":
|
||||
return import("../locales/ua/translations.json");
|
||||
case "pl":
|
||||
return import("../locales/pl/translations.json");
|
||||
case "ko":
|
||||
return import("../locales/ko/translations.json");
|
||||
default:
|
||||
throw new Error(`Unsupported language: ${language}`);
|
||||
}
|
||||
|
|
@ -163,40 +175,6 @@ export class TranslationStore {
|
|||
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
|
||||
}
|
||||
|
||||
/** Checks if a language code is similar to any supported language */
|
||||
private findSimilarLanguage(lang: string): TLanguage | null {
|
||||
// Convert to lowercase for case-insensitive comparison
|
||||
const normalizedLang = lang.toLowerCase();
|
||||
|
||||
// Find a supported language that includes or is included in the browser language
|
||||
const similarLang = this.availableLanguages.find(
|
||||
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
|
||||
);
|
||||
|
||||
return similarLang ? similarLang.value : null;
|
||||
}
|
||||
|
||||
/** Gets the browser language based on the navigator.language */
|
||||
private getBrowserLanguage(): TLanguage {
|
||||
const browserLang = navigator.language;
|
||||
|
||||
// Check exact match first
|
||||
if (this.isValidLanguage(browserLang)) {
|
||||
return browserLang;
|
||||
}
|
||||
|
||||
// Check base language without region code
|
||||
const baseLang = browserLang.split("-")[0];
|
||||
if (this.isValidLanguage(baseLang)) {
|
||||
return baseLang as TLanguage;
|
||||
}
|
||||
|
||||
// Try to find a similar language
|
||||
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
|
||||
|
||||
return similarLang || FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache key for the given key and locale
|
||||
* @param key - the key to get the cache key for
|
||||
|
|
@ -281,7 +259,7 @@ export class TranslationStore {
|
|||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, lng);
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
|
||||
document.documentElement.lang = lng;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it" | "cs";
|
||||
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "zh-TW" | "ru" | "it" | "cs" | "sk" | "de" | "ua" | "pl" | "ko";
|
||||
|
||||
export interface ILanguageOption {
|
||||
label: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/logger",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Logger shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/propel",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/services",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
|
|
@ -10,6 +10,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@plane/constants": "*",
|
||||
"axios": "^1.7.9"
|
||||
"axios": "^1.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/shared-state",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Shared state shared across multiple apps internally",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/tailwind-config",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "tailwind.config.js",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"types": "./src/index.d.ts",
|
||||
|
|
|
|||
4
packages/types/src/inbox.d.ts
vendored
4
packages/types/src/inbox.d.ts
vendored
|
|
@ -66,8 +66,10 @@ export type TInboxIssueWithPagination = TInboxIssuePaginationInfo & {
|
|||
results: TInboxIssue[];
|
||||
};
|
||||
|
||||
export type TAnchors = { [key: string]: string };
|
||||
|
||||
export type TInboxForm = {
|
||||
anchor: string;
|
||||
anchors: TAnchors;
|
||||
id: string;
|
||||
is_in_app_enabled: boolean;
|
||||
is_form_enabled: boolean;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,13 @@ export type TIssueActivity = {
|
|||
new_identifier: string | undefined;
|
||||
epoch: number;
|
||||
issue_comment: string | null;
|
||||
source_data: {
|
||||
source: "IN_APP" | "FORM" | "EMAIL";
|
||||
source_email?: string;
|
||||
extra: {
|
||||
username?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TIssueActivityMap = {
|
||||
|
|
|
|||
4
packages/types/src/project/projects.d.ts
vendored
4
packages/types/src/project/projects.d.ts
vendored
|
|
@ -25,6 +25,7 @@ export interface IPartialProject {
|
|||
module_view: boolean;
|
||||
page_view: boolean;
|
||||
inbox_view: boolean;
|
||||
guest_view_all_features?: boolean;
|
||||
project_lead?: IUserLite | string | null;
|
||||
// Timestamps
|
||||
created_at?: Date;
|
||||
|
|
@ -46,11 +47,8 @@ export interface IProject extends IPartialProject {
|
|||
default_state?: string | null;
|
||||
description?: string;
|
||||
estimate?: string | null;
|
||||
guest_view_all_features?: boolean;
|
||||
anchor?: string | null;
|
||||
is_favorite?: boolean;
|
||||
is_issue_type_enabled?: boolean;
|
||||
is_time_tracking_enabled?: boolean;
|
||||
members?: string[];
|
||||
network?: number;
|
||||
timezone?: string;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/typescript-config",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
|
@ -71,10 +71,7 @@
|
|||
"postcss-cli": "^11.0.0",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"storybook": "^8.1.1",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ import { cn } from "../helpers";
|
|||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||
|
||||
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => (
|
||||
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
|
||||
const thirtyYearsFromNowFirstDay = new Date(currentYear + 30, 11, 31);
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
|
|
@ -63,16 +68,16 @@ export const Calendar = ({ className, classNames, showOutsideDays = true, ...pro
|
|||
<ChevronLeft
|
||||
className={cn(
|
||||
"size-4",
|
||||
{
|
||||
"rotate-180": props.orientation === "right",
|
||||
"-rotate-90": props.orientation === "down",
|
||||
},
|
||||
{ "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
startMonth={thirtyYearsAgoFirstDay}
|
||||
endMonth={thirtyYearsFromNowFirstDay}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@plane/utils",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"description": "Helper functions shared across multiple apps internally",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/zxcvbn": "^4.4.5",
|
||||
"tsup": "^7.2.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||
<link rel="icon" type="image/png" sizes="16x16" href={`${SPACE_BASE_PATH}/favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${SPACE_BASE_PATH}/site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</head>
|
||||
<body>
|
||||
<AppProvider>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
|||
})}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] max-h-[50vh] border border-gray-100 rounded-md pl-3 pb-3 overflow-y-scroll"
|
||||
editorClassName="min-h-[100px] max-h-[50vh] border-[0.5px] border-custom-border-200 rounded-md pl-3 py-2 overflow-y-scroll"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { SignalHigh } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import { TIssuePriorities } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { getIssuePriorityFilters } from "@plane/utils";
|
||||
import { cn, getIssuePriorityFilters } from "@plane/utils";
|
||||
|
||||
export const IssueBlockPriority = ({
|
||||
priority,
|
||||
|
|
@ -18,14 +19,47 @@ export const IssueBlockPriority = ({
|
|||
const { t } = useTranslation();
|
||||
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
|
||||
|
||||
const priorityClasses = {
|
||||
urgent: "bg-red-600/10 text-red-600 border-red-600 px-1",
|
||||
high: "bg-orange-500/20 text-orange-950 border-orange-500",
|
||||
medium: "bg-yellow-500/20 text-yellow-950 border-yellow-500",
|
||||
low: "bg-custom-primary-100/20 text-custom-primary-950 border-custom-primary-100",
|
||||
none: "hover:bg-custom-background-80 border-custom-border-300",
|
||||
};
|
||||
|
||||
if (priority_detail === null) return <></>;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
|
||||
<div className="flex items-center relative w-full h-full">
|
||||
<div className={`grid h-5 w-5 place-items-center rounded border-[0.5px] gap-2 ${priority_detail?.className}`}>
|
||||
<span className="material-symbols-rounded text-sm">{priority_detail?.icon}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
||||
priorityClasses[priority ?? "none"],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": !shouldShowName,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-600/10 border-red-600": priority === "urgent" && shouldShowName,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{priority ? (
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": !shouldShowName,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": !shouldShowName && priority === "high",
|
||||
"translate-x-0.5": !shouldShowName && priority === "medium",
|
||||
"translate-x-1": !shouldShowName && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<SignalHigh className="size-3" />
|
||||
)}
|
||||
{shouldShowName && <span className="pl-2 text-sm">{t(priority_detail?.titleTranslationKey || "")}</span>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "space",
|
||||
"version": "0.25.2",
|
||||
"version": "0.25.3",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"@plane/ui": "*",
|
||||
"@plane/services": "*",
|
||||
"@sentry/nextjs": "^8.54.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.8.3",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.0.11",
|
||||
|
|
|
|||
2
space/public/robots.txt
Normal file
2
space/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -38,7 +38,7 @@ const ProjectCyclesPage = observer(() => {
|
|||
// derived values
|
||||
const totalCycles = currentProjectCycleIds?.length ?? 0;
|
||||
const project = projectId ? getProjectById(projectId?.toString()) : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - ${t("cycles.label", { count: 2 })}` : undefined;
|
||||
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined;
|
||||
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
const hasMemberLevelPermission = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
|
|
|
|||
|
|
@ -16,14 +16,16 @@ import { generateWorkItemLink } from "@/helpers/issue.helper";
|
|||
// plane web components
|
||||
import { IssueIdentifier } from "@/plane-web/components/issues";
|
||||
|
||||
export const commandGroups: {
|
||||
export type TCommandGroups = {
|
||||
[key: string]: {
|
||||
icon: JSX.Element | null;
|
||||
itemName: (item: any) => React.ReactNode;
|
||||
path: (item: any, projectId: string | undefined) => string;
|
||||
title: string;
|
||||
};
|
||||
} = {
|
||||
};
|
||||
|
||||
export const commandGroups: TCommandGroups = {
|
||||
cycle: {
|
||||
icon: <ContrastIcon className="h-3 w-3" />,
|
||||
itemName: (cycle: IWorkspaceDefaultSearchResult) => (
|
||||
3
web/ce/components/command-palette/index.ts
Normal file
3
web/ce/components/command-palette/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./actions";
|
||||
export * from "./modals";
|
||||
export * from "./helpers";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./values";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./update";
|
||||
|
|
@ -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> = () => <></>;
|
||||
|
|
@ -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> = () => <></>;
|
||||
|
|
@ -3,3 +3,4 @@ export * from "./issue-properties-activity";
|
|||
export * from "./issue-type-switcher";
|
||||
export * from "./issue-type-activity";
|
||||
export * from "./parent-select-root";
|
||||
export * from "./issue-creator";
|
||||
|
|
|
|||
36
web/ce/components/issues/issue-details/issue-creator.tsx
Normal file
36
web/ce/components/issues/issue-details/issue-creator.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ import { attachInstruction, extractInstruction } from "@atlaskit/pragmatic-drag-
|
|||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { Eye, EyeClosed } from "lucide-react";
|
||||
import { Pin, PinOff } from "lucide-react";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
|
|
@ -197,16 +197,16 @@ export const ExtendedSidebarItem: FC<TExtendedSidebarItemProps> = observer((prop
|
|||
</div>
|
||||
)}
|
||||
{isPinned ? (
|
||||
<Tooltip tooltipContent="Hide tab">
|
||||
<Eye
|
||||
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-300 outline-none"
|
||||
<Tooltip tooltipContent="Unpin">
|
||||
<PinOff
|
||||
className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
|
||||
onClick={() => unPinNavigationItem(workspaceSlug.toString(), item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip tooltipContent="Show tab">
|
||||
<EyeClosed
|
||||
className="size-4 flex-shrink-0 hover:text-custom-text-200 text-custom-text-400 outline-none"
|
||||
<Tooltip tooltipContent="Pin">
|
||||
<Pin
|
||||
className="size-3.5 flex-shrink-0 hover:text-custom-text-300 outline-none text-custom-text-400"
|
||||
onClick={() => pinNavigationItem(workspaceSlug.toString(), item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ export type TProperties = {
|
|||
icon: ReactNode;
|
||||
isPro: boolean;
|
||||
isEnabled: boolean;
|
||||
renderChildren?: (
|
||||
currentProjectDetails: IProject,
|
||||
isAdmin: boolean,
|
||||
handleSubmit: (featureKey: string, featureProperty: string) => Promise<void>,
|
||||
workspaceSlug: string
|
||||
) => ReactNode;
|
||||
renderChildren?: (currentProjectDetails: IProject, workspaceSlug: string) => ReactNode;
|
||||
};
|
||||
export type TFeatureList = {
|
||||
[key: string]: TProperties;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// types
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// lib
|
||||
import { store } from "@/lib/store-context";
|
||||
|
||||
export type TShouldRenderDisplayProperty = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -16,3 +18,13 @@ export const shouldRenderDisplayProperty = (props: TShouldRenderDisplayProperty)
|
|||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const shouldRenderColumn = (key: keyof IIssueDisplayProperties): boolean => {
|
||||
const isEstimateEnabled: boolean = store.projectRoot.project.currentProjectDetails?.estimate !== null;
|
||||
switch (key) {
|
||||
case "estimate":
|
||||
return isEstimateEnabled;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
4
web/ce/store/issue/helpers/base-issue.store.ts
Normal file
4
web/ce/store/issue/helpers/base-issue.store.ts
Normal 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);
|
||||
1
web/ce/store/project-inbox.store.ts
Normal file
1
web/ce/store/project-inbox.store.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "@/store/inbox/project-inbox.store";
|
||||
|
|
@ -97,7 +97,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.i18n_label} value={month.value}>
|
||||
<span className="text-sm">{t(month.i18n_label, { month: month.value })}</span>
|
||||
<span className="text-sm">{t(month.i18n_label, { months: month.value })}</span>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
|||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
{t("customize_time_range")}
|
||||
{t("common.customize_time_range")}
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.i18n_label} value={month.value}>
|
||||
{t(month.i18n_label, { month: month.value })}
|
||||
{t(month.i18n_label, { months: month.value })}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
<button
|
||||
|
|
@ -132,7 +132,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
|||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
{t("customize_time_range")}
|
||||
{t("common.customize_time_range")}
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
import { Command } from "cmdk";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
// plane imports
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
// helpers
|
||||
import { commandGroups } from "@/components/command-palette";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web imports
|
||||
import { commandGroups } from "@/plane-web/components/command-palette";
|
||||
|
||||
type Props = {
|
||||
closePalette: () => void;
|
||||
|
|
@ -25,9 +25,9 @@ export const CommandPaletteSearchResults: React.FC<Props> = (props) => {
|
|||
return (
|
||||
<>
|
||||
{Object.keys(results.results).map((key) => {
|
||||
// TODO: add type for results
|
||||
const section = (results.results as any)[key];
|
||||
const currentSection = commandGroups[key];
|
||||
|
||||
if (!currentSection) return null;
|
||||
if (section.length > 0) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import useSWR from "swr";
|
|||
import { CommandIcon, FolderPlus, Search, Settings, X } from "lucide-react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_DEFAULT_SEARCH_RESULT } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IWorkspaceSearchResults } from "@plane/types";
|
||||
import { LayersIcon, Loader, ToggleSwitch } from "@plane/ui";
|
||||
|
|
@ -58,9 +58,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [results, setResults] = useState<IWorkspaceSearchResults>({
|
||||
results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
|
||||
});
|
||||
const [results, setResults] = useState<IWorkspaceSearchResults>(WORKSPACE_DEFAULT_SEARCH_RESULT);
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
const [searchInIssue, setSearchInIssue] = useState(false);
|
||||
|
|
@ -151,9 +149,7 @@ export const CommandModal: React.FC = observer(() => {
|
|||
setIsSearching(false);
|
||||
});
|
||||
} else {
|
||||
setResults({
|
||||
results: { workspace: [], project: [], issue: [], cycle: [], module: [], issue_view: [], page: [] },
|
||||
});
|
||||
setResults(WORKSPACE_DEFAULT_SEARCH_RESULT);
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@ export * from "./actions";
|
|||
export * from "./shortcuts-modal";
|
||||
export * from "./command-modal";
|
||||
export * from "./command-palette";
|
||||
export * from "./helpers";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ import { useParams, usePathname, useSearchParams } from "next/navigation";
|
|||
import { useForm } from "react-hook-form";
|
||||
import { Eye, Users } from "lucide-react";
|
||||
// types
|
||||
import { CYCLE_FAVORITED, CYCLE_UNFAVORITED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import {
|
||||
CYCLE_FAVORITED,
|
||||
CYCLE_UNFAVORITED,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
} from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -15,8 +22,6 @@ import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, s
|
|||
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
|
||||
import { DateRangeDropdown } from "@/components/dropdowns";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
// constants
|
||||
// helpers
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
|
|
@ -59,6 +64,12 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
const { captureEvent } = useEventTracker();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
// form
|
||||
|
|
@ -91,6 +102,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
|||
|
||||
const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then(
|
||||
() => {
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||
captureEvent(CYCLE_FAVORITED, {
|
||||
cycle_id: cycleId,
|
||||
element: "List layout",
|
||||
|
|
|
|||
|
|
@ -21,13 +21,27 @@ export const IssueDefaultActivity: FC<TIssueDefaultActivity> = observer((props)
|
|||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
const source = activity.source_data?.source;
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
activityId={activityId}
|
||||
icon={<LayersIcon width={14} height={14} className="text-custom-text-200" aria-hidden="true" />}
|
||||
ends={ends}
|
||||
>
|
||||
<>{activity.verb === "created" ? " created the work item." : " deleted a work item."}</>
|
||||
<>
|
||||
{activity.verb === "created" ? (
|
||||
source && source !== "IN_APP" ? (
|
||||
<span>
|
||||
created the work item via <span className="font-medium">{source.toLowerCase()}</span>.
|
||||
</span>
|
||||
) : (
|
||||
<span> created the work item.</span>
|
||||
)
|
||||
) : (
|
||||
<span> deleted a work item.</span>
|
||||
)}
|
||||
</>
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useIssueDetail } from "@/hooks/store";
|
|||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// ui
|
||||
// components
|
||||
import { IssueCreatorDisplay } from "@/plane-web/components/issues";
|
||||
import { IssueUser } from "../";
|
||||
// helpers
|
||||
|
||||
|
|
@ -41,7 +42,11 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
|||
{icon ? icon : <Network className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
<div className="w-full truncate text-custom-text-200">
|
||||
{!activity?.field && activity?.verb === "created" ? (
|
||||
<IssueCreatorDisplay activityId={activityId} customUserName={customUserName} />
|
||||
) : (
|
||||
<IssueUser activityId={activityId} customUserName={customUserName} />
|
||||
)}
|
||||
<span> {children} </span>
|
||||
<span>
|
||||
<Tooltip
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
className="fixed right-0 z-[5] h-full w-full min-w-[300px] overflow-hidden border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
|
||||
className="fixed right-0 z-[5] h-full w-full min-w-[300px] border-l border-custom-border-200 bg-custom-sidebar-background-100 py-5 sm:w-1/2 md:relative md:w-1/3 lg:min-w-80 xl:min-w-96"
|
||||
style={issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
||||
>
|
||||
<IssueDetailsSidebar
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
|||
// hooks
|
||||
import { useProjectEstimates, useIssueDetail, useProject, useProjectState, useMember } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values";
|
||||
import { IssueParentSelectRoot, IssueWorklogProperty } from "@/plane-web/components/issues";
|
||||
// components
|
||||
import { WorkItemAdditionalSidebarProperties } from "@/plane-web/components/issues/issue-details/additional-properties";
|
||||
import type { TIssueOperations } from "./root";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -293,15 +293,14 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||
disabled={!isEditable}
|
||||
/>
|
||||
|
||||
{issue.type_id && (
|
||||
<IssueAdditionalPropertyValuesUpdate
|
||||
issueId={issueId}
|
||||
issueTypeId={issue.type_id}
|
||||
<WorkItemAdditionalSidebarProperties
|
||||
workItemId={issue.id}
|
||||
workItemTypeId={issue.type_id}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
isDisabled={!isEditable}
|
||||
isEditable={isEditable}
|
||||
isPeekView
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { IIssueDisplayProperties, TIssue } from "@plane/types";
|
|||
import { useEventTracker } from "@/hooks/store";
|
||||
// components
|
||||
import { SPREADSHEET_COLUMNS } from "@/plane-web/components/issues/issue-layouts/utils";
|
||||
import { shouldRenderColumn } from "@/plane-web/helpers/issue-filter.helper";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
// utils
|
||||
|
||||
|
|
@ -20,13 +21,13 @@ type Props = {
|
|||
};
|
||||
|
||||
export const IssueColumn = observer((props: Props) => {
|
||||
const { displayProperties, issueDetail, disableUserActions, property, updateIssue, isEstimateEnabled } = props;
|
||||
const { displayProperties, issueDetail, disableUserActions, property, updateIssue } = props;
|
||||
// router
|
||||
const pathname = usePathname();
|
||||
const tableCellRef = useRef<HTMLTableCellElement | null>(null);
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
const shouldRenderProperty = shouldRenderColumn(property);
|
||||
|
||||
const Column = SPREADSHEET_COLUMNS[property];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useRef } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
//components
|
||||
import { shouldRenderColumn } from "@/plane-web/helpers/issue-filter.helper";
|
||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||
import { HeaderColumn } from "./columns/header-column";
|
||||
|
||||
|
|
@ -15,19 +16,12 @@ interface Props {
|
|||
isEpic?: boolean;
|
||||
}
|
||||
export const SpreadsheetHeaderColumn = observer((props: Props) => {
|
||||
const {
|
||||
displayProperties,
|
||||
displayFilters,
|
||||
property,
|
||||
isEstimateEnabled,
|
||||
handleDisplayFilterUpdate,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
const { displayProperties, displayFilters, property, handleDisplayFilterUpdate, isEpic = false } = props;
|
||||
|
||||
//hooks
|
||||
const tableHeaderCellRef = useRef<HTMLTableCellElement | null>(null);
|
||||
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
const shouldRenderProperty = shouldRenderColumn(property);
|
||||
|
||||
return (
|
||||
<WithDisplayPropertiesHOC
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"
|
|||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { useIssueDetail, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueAdditionalPropertyValuesUpdate } from "@/plane-web/components/issue-types/values";
|
||||
import { IssueParentSelectRoot, IssueWorklogProperty } from "@/plane-web/components/issues";
|
||||
import { WorkItemAdditionalSidebarProperties } from "@/plane-web/components/issues/issue-details/additional-properties";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -291,15 +291,14 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{issue.type_id && (
|
||||
<IssueAdditionalPropertyValuesUpdate
|
||||
issueId={issueId}
|
||||
issueTypeId={issue.type_id}
|
||||
<WorkItemAdditionalSidebarProperties
|
||||
workItemId={issue.id}
|
||||
workItemTypeId={issue.type_id}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
isDisabled={disabled}
|
||||
isEditable={!disabled}
|
||||
isPeekView
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import {
|
|||
MODULE_UNFAVORITED,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
} from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { IModule } from "@plane/types";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -30,7 +32,6 @@ import { DateRangeDropdown } from "@/components/dropdowns";
|
|||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
import { ModuleStatusDropdown } from "@/components/modules/module-status-dropdown";
|
||||
// constants
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { generateQueryParams } from "@/helpers/router.helper";
|
||||
|
|
@ -59,6 +60,9 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||
const { getUserDetails } = useMember();
|
||||
const { captureEvent } = useEventTracker();
|
||||
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
const isEditingAllowed = allowPermissions(
|
||||
|
|
@ -76,6 +80,7 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
|||
|
||||
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
|
||||
() => {
|
||||
if (!storedValue) toggleFavoriteMenu(true);
|
||||
captureEvent(MODULE_FAVORITED, {
|
||||
module_id: moduleId,
|
||||
element: "Grid layout",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import {
|
|||
MODULE_UNFAVORITED,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
} from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IModule } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -45,6 +47,8 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
|||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
|
||||
// derived values
|
||||
|
||||
const moduleStatus = MODULE_STATUS.find((status) => status.value === moduleDetails.status);
|
||||
|
|
@ -63,6 +67,8 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
|
|||
|
||||
const addToFavoritePromise = addModuleToFavorites(workspaceSlug.toString(), projectId.toString(), moduleId).then(
|
||||
() => {
|
||||
// open favorites menu if closed
|
||||
if (!storedValue) toggleFavoriteMenu(true);
|
||||
captureEvent(MODULE_FAVORITED, {
|
||||
module_id: moduleId,
|
||||
element: "Grid layout",
|
||||
|
|
|
|||
|
|
@ -90,20 +90,17 @@ export const ModulesListView: React.FC = observer(() => {
|
|||
|
||||
return (
|
||||
<ContentWrapper variant={ERowVariant.HUGGING}>
|
||||
<div className="size-full flex justify-between">
|
||||
{displayFilters?.layout === "list" && (
|
||||
<div className="flex h-full w-full justify-between">
|
||||
<ListLayout>
|
||||
{filteredModuleIds.map((moduleId) => (
|
||||
<ModuleListItem key={moduleId} moduleId={moduleId} />
|
||||
))}
|
||||
</ListLayout>
|
||||
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
|
||||
</div>
|
||||
)}
|
||||
{displayFilters?.layout === "board" && (
|
||||
<Row className="flex h-full w-full justify-between py-page-y">
|
||||
<div
|
||||
className={`grid h-full w-full grid-cols-1 gap-6 overflow-y-auto ${
|
||||
<Row
|
||||
className={`size-full py-page-y grid grid-cols-1 gap-6 overflow-y-auto ${
|
||||
peekModule
|
||||
? "lg:grid-cols-1 xl:grid-cols-2 3xl:grid-cols-3"
|
||||
: "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4"
|
||||
|
|
@ -112,11 +109,17 @@ export const ModulesListView: React.FC = observer(() => {
|
|||
{filteredModuleIds.map((moduleId) => (
|
||||
<ModuleCardItem key={moduleId} moduleId={moduleId} />
|
||||
))}
|
||||
</div>
|
||||
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
|
||||
</Row>
|
||||
)}
|
||||
{displayFilters?.layout === "gantt" && <ModulesListGanttChartView />}
|
||||
{displayFilters?.layout === "gantt" && (
|
||||
<div className="size-full overflow-hidden">
|
||||
<ModulesListGanttChartView />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
<ModulePeekOverview projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
</ContentWrapper>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// constants
|
||||
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
||||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// plane hooks
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
// ui
|
||||
import { ArchiveIcon, FavoriteStar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
|
|
@ -37,6 +41,11 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
|||
} = page;
|
||||
// use online status
|
||||
const { isOnline } = useOnlineStatus();
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
// favorite handler
|
||||
const handleFavorite = () => {
|
||||
if (is_favorite) {
|
||||
|
|
@ -48,13 +57,14 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
|||
})
|
||||
);
|
||||
} else {
|
||||
addToFavorites().then(() =>
|
||||
addToFavorites().then(() => {
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page added to favorites.",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import Link from "next/link";
|
|||
import { useParams } from "next/navigation";
|
||||
import { ArchiveRestoreIcon, Check, ExternalLink, LinkIcon, Lock, Settings, Trash2, UserPlus } from "lucide-react";
|
||||
// types
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import type { IProject } from "@plane/types";
|
||||
// ui
|
||||
import {
|
||||
|
|
@ -68,6 +69,11 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
const hasMemberRole = project.member_role === EUserPermissions.MEMBER;
|
||||
// archive
|
||||
const isArchived = !!project.archived_at;
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
|
@ -78,6 +84,10 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
success: {
|
||||
title: "Success!",
|
||||
message: () => "Project added to favorites.",
|
||||
actionItems: () => {
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||
return <></>;
|
||||
},
|
||||
},
|
||||
error: {
|
||||
title: "Error!",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
|||
<h4 className="text-sm font-medium leading-5">{t(featureItem.key)}</h4>
|
||||
{featureItem.isPro && (
|
||||
<Tooltip tooltipContent="Pro feature" position="top">
|
||||
<UpgradeBadge />
|
||||
<UpgradeBadge className="rounded" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -103,8 +103,7 @@ export const ProjectFeaturesList: FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<div className="pl-14">
|
||||
{currentProjectDetails?.[featureItem.property as keyof IProject] &&
|
||||
featureItem.renderChildren &&
|
||||
featureItem.renderChildren(currentProjectDetails, isAdmin, handleSubmit, workspaceSlug)}
|
||||
featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { Earth, Lock } from "lucide-react";
|
||||
// types
|
||||
import { EViewAccess, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EViewAccess, EUserPermissions, EUserPermissionsLevel, IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
||||
import { useLocalStorage } from "@plane/hooks";
|
||||
import { IProjectView } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, FavoriteStar } from "@plane/ui";
|
||||
// components
|
||||
import { DeleteProjectViewModal, CreateUpdateProjectViewModal, ViewQuickActions } from "@/components/views";
|
||||
// constants
|
||||
// helpers
|
||||
import { calculateTotalFilters } from "@/helpers/filter.helper";
|
||||
import { getPublishViewLink } from "@/helpers/project-views.helpers";
|
||||
|
|
@ -37,6 +37,12 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
const { addViewToFavorites, removeViewFromFavorites } = useProjectView();
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
|
||||
// derived values
|
||||
const isEditingAllowed = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
|
|
@ -50,10 +56,11 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
const publishLink = getPublishViewLink(view?.anchor);
|
||||
|
||||
// handlers
|
||||
const handleAddToFavorites = () => {
|
||||
const handleAddToFavorites = async () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
|
||||
await addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
|
||||
if (!isFavoriteOpen) toggleFavoriteMenu(true);
|
||||
};
|
||||
|
||||
const handleRemoveFromFavorites = () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
{"."}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./item";
|
||||
export * from "./options";
|
||||
export * from "./content";
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@ import { NotificationOption } from "@/components/workspace-notifications";
|
|||
import { cn } from "@/helpers/common.helper";
|
||||
import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
|
||||
import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useNotification, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { useIssueDetail, useNotification, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { NotificationContent } from "./content";
|
||||
|
||||
type TNotificationItem = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -26,6 +25,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
|||
const { currentSelectedNotificationId, setCurrentSelectedNotificationId } = useWorkspaceNotifications();
|
||||
const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// states
|
||||
const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
|
||||
const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
|
||||
|
|
@ -33,6 +33,7 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
|||
// derived values
|
||||
const projectId = notification?.project || undefined;
|
||||
const issueId = notification?.data?.issue?.id || undefined;
|
||||
const workspace = getWorkspaceBySlug(workspaceSlug);
|
||||
|
||||
const notificationField = notification?.data?.issue_activity.field || undefined;
|
||||
const notificationTriggeredBy = notification.triggered_by_details || undefined;
|
||||
|
|
@ -57,7 +58,8 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
|||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <></>;
|
||||
if (!workspaceSlug || !notificationId || !notification?.id || !notificationField || !workspace?.id || !projectId)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<Row
|
||||
|
|
@ -88,56 +90,12 @@ export const NotificationItem: FC<TNotificationItem> = observer((props) => {
|
|||
<div className="w-full space-y-1 -mt-2">
|
||||
<div className="relative flex items-center gap-3 h-8">
|
||||
<div className="w-full overflow-hidden whitespace-normal break-all truncate line-clamp-1 text-sm text-custom-text-100">
|
||||
{!notification.message ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
{notificationTriggeredBy?.is_bot
|
||||
? notificationTriggeredBy?.first_name
|
||||
: notificationTriggeredBy?.display_name}{" "}
|
||||
</span>
|
||||
{!["comment", "archived_at"].includes(notificationField) && notification?.data?.issue_activity.verb}{" "}
|
||||
{notificationField === "comment"
|
||||
? "commented"
|
||||
: notificationField === "archived_at"
|
||||
? notification?.data?.issue_activity.new_value === "restore"
|
||||
? "restored the work item"
|
||||
: "archived the work item"
|
||||
: notificationField === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notificationField)}{" "}
|
||||
{notification?.data?.issue_activity.verb !== "deleted" && (
|
||||
<>
|
||||
{!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notificationField !== "None" ? (
|
||||
notificationField !== "comment" ? (
|
||||
notificationField === "target_date" ? (
|
||||
renderFormattedDate(notification?.data?.issue_activity.new_value)
|
||||
) : notificationField === "attachment" ? (
|
||||
"the work item"
|
||||
) : notificationField === "description" ? (
|
||||
stripAndTruncateHTML(notification?.data?.issue_activity.new_value || "", 55)
|
||||
) : notificationField === "archived_at" ? null : (
|
||||
notification?.data?.issue_activity.new_value
|
||||
)
|
||||
) : (
|
||||
<span>
|
||||
{sanitizeCommentForNotification(
|
||||
notification?.data?.issue_activity.new_value ?? undefined
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
"the work item and assigned it to you."
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="semi-bold">{notification.message}</span>
|
||||
)}
|
||||
<NotificationContent
|
||||
notification={notification}
|
||||
workspaceId={workspace.id}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
<NotificationOption
|
||||
workspaceSlug={workspaceSlug}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import { ChevronRight, FolderPlus } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { IFavorite } from "@plane/types";
|
||||
|
|
@ -54,7 +55,7 @@ export const SidebarFavoritesMenu = observer(() => {
|
|||
const { isMobile } = usePlatformOS();
|
||||
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>("is_favorite_menu_open", false);
|
||||
const { setValue: toggleFavoriteMenu, storedValue } = useLocalStorage<boolean>(IS_FAVORITE_MENU_OPEN, false);
|
||||
// derived values
|
||||
const isFavoriteMenuOpen = !!storedValue;
|
||||
// refs
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import { IProjectInboxStore } from "@/store/inbox/project-inbox.store";
|
||||
import { IProjectInboxStore } from "@/plane-web/store/project-inbox.store";
|
||||
|
||||
export const useProjectInbox = (): IProjectInboxStore => {
|
||||
const context = useContext(StoreContext);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useMemo } from "react";
|
||||
// plane constants
|
||||
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
||||
// plane editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
|
|
@ -11,6 +13,8 @@ import { copyUrlToClipboard } from "@/helpers/string.helper";
|
|||
import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions";
|
||||
// store types
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
// local storage
|
||||
import useLocalStorage from "./use-local-storage";
|
||||
|
||||
export type TPageOperations = {
|
||||
toggleLock: () => void;
|
||||
|
|
@ -46,6 +50,11 @@ export const usePageOperations = (
|
|||
} = page;
|
||||
// collaborative actions
|
||||
const { executeCollaborativeAction } = useCollaborativePageActions(props);
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isfavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
// page operations
|
||||
const pageOperations: TPageOperations = useMemo(() => {
|
||||
const pageLink = getRedirectionLink();
|
||||
|
|
@ -141,13 +150,14 @@ export const usePageOperations = (
|
|||
})
|
||||
);
|
||||
} else {
|
||||
addToFavorites().then(() =>
|
||||
addToFavorites().then(() => {
|
||||
if (!isfavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page added to favorites.",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleLock: async () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as Comlink from "comlink";
|
|||
import set from "lodash/set";
|
||||
// plane
|
||||
import { EIssueGroupBYServerToProperty } from "@plane/constants";
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TIssue, TIssueParams } from "@plane/types";
|
||||
// lib
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
// services
|
||||
|
|
@ -15,6 +15,7 @@ import { addIssuesBulk, syncDeletesToLocal } from "./utils/load-issues";
|
|||
import { loadWorkSpaceData } from "./utils/load-workspace";
|
||||
import { issueFilterCountQueryConstructor, issueFilterQueryConstructor } from "./utils/query-constructor";
|
||||
import { runQuery } from "./utils/query-executor";
|
||||
import { sanitizeWorkItemQueries } from "./utils/query-sanitizer.ts";
|
||||
import { createTables } from "./utils/tables";
|
||||
import { clearOPFS, getGroupedIssueResults, getSubGroupedIssueResults, log, logError } from "./utils/utils";
|
||||
|
||||
|
|
@ -269,7 +270,12 @@ export class Storage {
|
|||
return issue.updated_at;
|
||||
};
|
||||
|
||||
getIssues = async (workspaceSlug: string, projectId: string, queries: any, config: any) => {
|
||||
getIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
queries: Partial<Record<TIssueParams, string | boolean>> | undefined,
|
||||
config: any
|
||||
) => {
|
||||
log("#### Queries", queries);
|
||||
|
||||
const currentProjectStatus = this.getStatus(projectId);
|
||||
|
|
@ -294,11 +300,12 @@ export class Storage {
|
|||
}
|
||||
}
|
||||
|
||||
const { cursor, group_by, sub_group_by } = queries;
|
||||
const sanitizedQueries = sanitizeWorkItemQueries(workspaceSlug, projectId, queries);
|
||||
const { cursor, group_by, sub_group_by } = sanitizedQueries || {};
|
||||
|
||||
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, sanitizedQueries);
|
||||
log("#### Query", query);
|
||||
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
|
||||
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, sanitizedQueries);
|
||||
const start = performance.now();
|
||||
let issuesRaw: any[] = [];
|
||||
let count: any[];
|
||||
|
|
@ -313,7 +320,7 @@ export class Storage {
|
|||
|
||||
const { total_count } = count[0];
|
||||
|
||||
const [pageSize, page, offset] = cursor.split(":");
|
||||
const [pageSize, page, offset] = cursor && typeof cursor === "string" ? cursor.split(":") : [];
|
||||
|
||||
const groupByProperty: string =
|
||||
EIssueGroupBYServerToProperty[group_by as keyof typeof EIssueGroupBYServerToProperty];
|
||||
|
|
|
|||
41
web/core/local-db/utils/query-sanitizer.ts.ts
Normal file
41
web/core/local-db/utils/query-sanitizer.ts.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// types
|
||||
import { TInboxIssue } from "@plane/constants";
|
||||
import type { TIssue, TInboxIssueWithPagination, TInboxForm } from "@plane/types";
|
||||
import type { TIssue, TInboxIssueWithPagination } from "@plane/types";
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
// helpers
|
||||
|
|
@ -76,28 +76,4 @@ export class InboxIssueService extends APIService {
|
|||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrievePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updatePublishForm(workspaceSlug: string, projectId: string, data: Partial<TInboxForm>): Promise<TInboxForm> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/intake-settings/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async regeneratePublishForm(workspaceSlug: string, projectId: string): Promise<TInboxForm> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/publish-intake-regenerate/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { EIssueServiceType } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
TIssueParams,
|
||||
type IIssueDisplayProperties,
|
||||
type TBulkOperationsPayload,
|
||||
type TIssue,
|
||||
|
|
@ -75,7 +76,12 @@ export class IssueService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async getIssues(workspaceSlug: string, projectId: string, queries?: any, config = {}): Promise<TIssuesResponse> {
|
||||
async getIssues(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
queries?: Partial<Record<TIssueParams, string | boolean>>,
|
||||
config = {}
|
||||
): Promise<TIssuesResponse> {
|
||||
if (getIssuesShouldFallbackToServer(queries) || this.serviceType !== EIssueServiceType.ISSUES) {
|
||||
return await this.getIssuesFromServer(workspaceSlug, projectId, queries, config);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,9 +111,10 @@ export class FavoriteStore implements IFavoriteStore {
|
|||
* @returns Promise<IFavorite>
|
||||
*/
|
||||
addFavorite = async (workspaceSlug: string, data: Partial<IFavorite>) => {
|
||||
const id = uuidv4();
|
||||
data = { ...data, parent: null, is_folder: data.entity_type === "folder" };
|
||||
|
||||
if (data.entity_identifier && this.entityMap[data.entity_identifier]) return this.entityMap[data.entity_identifier];
|
||||
const id = uuidv4();
|
||||
try {
|
||||
// optimistic addition
|
||||
runInAction(() => {
|
||||
|
|
@ -271,6 +272,7 @@ export class FavoriteStore implements IFavoriteStore {
|
|||
* @returns Promise<void>
|
||||
*/
|
||||
deleteFavorite = async (workspaceSlug: string, favoriteId: string) => {
|
||||
if (!this.favoriteMap[favoriteId]) return;
|
||||
const parent = this.favoriteMap[favoriteId].parent;
|
||||
const children = this.groupedFavorites[favoriteId].children;
|
||||
const entity_identifier = this.favoriteMap[favoriteId].entity_identifier;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue