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