Compare commits

...

50 Commits

Author SHA1 Message Date
sriram veeraghanta
72307ec100 chore: update package version 2025-03-21 17:00:14 +05:30
Prateek Shourya
94bf90dac5 [WEB-3597] fix: guest work item view access when hyper mode is enabled (#6785)
* [WEB-3597] fix: guest work item view access when hyper mode is enabled

* fix: only show work item created by the guest user if the guest_view_all_features is disabled
2025-03-20 19:43:40 +05:30
Anmol Singh Bhatia
b0e941e4e2 [WEB-3590] chore: sidebar enhancements (#6780)
* chore: implement optimistic update for extended sidebar item

* chore: replace eye icon with pin icon for show/hide functionality

* chore: code refactor

* chore: code refactor
2025-03-20 16:43:45 +05:30
Akshita Goyal
e22265dc93 fix: intake refactor (#6698)
* fix: refactor

* fix: refactor

* fix: type

* chore: added source data in intake

* fix: css

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-20 14:06:36 +05:30
Sangeetha
075c234385 [WEB-3286] fix: allow admins to delete other admins views (#6769)
* fix: allow admins to delete other admins views

* fix: allow admins to delete other admins views
2025-03-20 14:04:28 +05:30
Vamsi Krishna
bc539e0d01 [WEB-3175]fix: favorites menu (#6773)
* fix: favrotites menu open

* fix: open fav menu on starring projec

* chore: added constant for hardcoded text
2025-03-20 14:03:24 +05:30
Nikhil
04fb13cbca chore: update celery cron configurations (#6776) 2025-03-20 14:02:55 +05:30
Vamsi Krishna
ca5cf27957 [WEB-3262]fix: incomplete activity render for activity in notifications (#6777)
* fix: incomplete activity render for activity in notifications

* fix: handled content overflow for long notification messages
2025-03-20 14:02:06 +05:30
Vamsi Krishna
433682e913 fix: space app icons display (#6784) 2025-03-20 13:58:48 +05:30
Aaryan Khandelwal
f181b671f3 chore: update year range in the calendar component (#6781) 2025-03-19 19:06:58 +05:30
Prateek Shourya
f82d4a9ead [WEB-3589] improvement: reset language to default on sign out (#6775) 2025-03-19 14:42:19 +05:30
Anmol Singh Bhatia
3f22642732 [WEB-3580] fix: automations settings translation (#6767) 2025-03-18 16:34:58 +05:30
Anmol Singh Bhatia
e339b7ad8f [WEB-3545] feat: language translations (#6762)
* feat(translations): add Korean translation (#6755)

* feat(translations): init Korean translation

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

* feat(translations): add rough Korean translation

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

---------

Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>

* feat(translations): add Slovak, Deutsch, Ukrainian and Polish translation (#6743)

* feat(translation): add Slovak translation

* feat(translation): add Slovak translation for workspace

* feat(translation): improved Slovak translation for views

* feat(translation): add Deutsch translation

* feat(translation): add Ukrainian translation

* feat(translation): add Polish translation

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* fix: project cycle translations

* fix: build error

* feat: Add zh-TW Traditional Chinese locale (#6764)

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>

* chore: zh-TW core translation updated

---------

Co-authored-by: NavyStack <navystack@askfront.com>
Co-authored-by: FVOCI <150913557+fvoci@users.noreply.github.com>
Co-authored-by: Ján Regeš <jan.reges@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Peter Dave Hello <hsu@peterdavehello.org>
2025-03-18 13:35:46 +05:30
Vamsi Krishna
d4991b9a48 chore: issue filters refactor (#6742)
* chore: issue filters refactor

* chore: update helper funciton implementation

* chore: removed redundant components
2025-03-17 15:45:34 +05:30
Prateek Shourya
1bf683e044 improvement: command palette search results (#6761) 2025-03-17 15:45:03 +05:30
Akshita Goyal
807148671f fix: build (#6760) 2025-03-17 14:38:46 +05:30
Akshita Goyal
d2b81ad2da fix: favorite mutation on quick actions (#6741)
* fix: fav mutation on quick actions

* fix: user favorite fetch

* fix: exist validation

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-17 14:14:45 +05:30
sriram veeraghanta
3d14c9d9fe fix: build test pull request changes 2025-03-17 00:38:51 +05:30
sriram veeraghanta
d7f40cf578 fix: remove files changed step from branch build workflow 2025-03-17 00:26:10 +05:30
Aaryan Khandelwal
b370ef72ee [RANTS-46] fix: modules list sidebar position (#6754) 2025-03-16 23:50:04 +05:30
sriram veeraghanta
0341205666 fix: live server dev port to 3100 2025-03-13 15:42:43 +05:30
sriram veeraghanta
41fe7a59eb chore: axios package update 2025-03-13 14:28:40 +05:30
sriram veeraghanta
dcbee45d82 chore: updated package resolutions 2025-03-13 14:05:15 +05:30
Akshita Goyal
c3560c6586 fix: translation key (#6745) 2025-03-13 13:39:14 +05:30
Anmol Singh Bhatia
a477f55b23 [WEB-3509] chore: disable search indexing for space app (#6735) 2025-03-11 16:52:25 +05:30
dependabot[bot]
9ee1d8cb03 chore(deps): bump the npm_and_yarn group across 6 directories with 2 updates (#6737)
Bumps the npm_and_yarn group with 2 updates in the / directory: [axios](https://github.com/axios/axios) and [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /live directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/editor directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/hooks directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/ui directory: [tsup](https://github.com/egoist/tsup).
Bumps the npm_and_yarn group with 1 update in the /packages/utils directory: [tsup](https://github.com/egoist/tsup).


Updates `axios` from 1.7.9 to 1.8.2
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.9...v1.8.2)

Updates `tsup` from 7.3.0 to 8.3.5
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

Updates `tsup` from 7.3.0 to 8.4.0
- [Release notes](https://github.com/egoist/tsup/releases)
- [Commits](https://github.com/egoist/tsup/compare/v7.3.0...v8.3.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsup
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 01:14:37 +05:30
sriram veeraghanta
b478e36b59 fix: package version upgrade 2025-03-10 18:47:38 +05:30
Vipin Chaudhary
2a1cef0360 [PE-262] fix: add break-all to break words (#6734) 2025-03-10 18:37:37 +05:30
Anmol Singh Bhatia
52b9b12f74 fix: ambiguous class warning (#6733) 2025-03-10 18:12:51 +05:30
Vamsi Krishna
45ba8cbc83 fix: build errors (#6732) 2025-03-10 17:54:19 +05:30
Akshat Jain
4ce40fb3db chore: add env MINIO_ENDPOINT_SSL in docker compose file (#6731) 2025-03-10 17:39:07 +05:30
Nikhil
9ba2ed7d89 chore: make api rate limit configurable through environment variable (#6730) 2025-03-10 17:27:35 +05:30
Vipin Chaudhary
6157900b23 [PE-294] fix : route back on page delete (#6729) 2025-03-10 17:19:37 +05:30
Vamsi Krishna
a9045adf17 [WEB-3504]chore: issue properties refactor (#6724)
* chore: issue properties refactor

* chore: added export to props

* chore: updated component name
2025-03-10 17:14:46 +05:30
Akshat Jain
d1e462bb37 chore: Add env for handling uploads SSL Termination (#6722) 2025-03-10 15:45:11 +05:30
Anmol Singh Bhatia
7df63151b5 [WEB-3134] fix: plane logo rendering issue in safari (#6728) 2025-03-10 15:35:07 +05:30
Anmol Singh Bhatia
05b0716822 chore: cs translations updated (#6726) 2025-03-10 14:50:48 +05:30
Anmol Singh Bhatia
b75c9a8d8d [WEB-3401] fix: platform translations (#6727)
* fix: platform translations

* chore: common translation updated
2025-03-10 14:28:38 +05:30
Ján Regeš
099c5d50ee feat(translation): add Czech translation (#6725) 2025-03-10 12:41:55 +05:30
Prateek Shourya
a953013f70 [WEB-3489] improvement: add support to disable extensions in rich and lite text editor (#6721)
* [WEB-3489] improvement: add support to disable extensions in rich text editor

* improvements: disabled extensions prop for all editor components
2025-03-07 16:34:07 +05:30
sriram veeraghanta
a77fe7aa90 fix: django version upgrade to fix vulnerability 2025-03-07 13:35:54 +05:30
Aaryan Khandelwal
cb344ea1f5 refactor: favorites sidebar implementation (#6716)
* chore: code separation for favorites

* chore: error handling
2025-03-07 13:17:13 +05:30
Nikhil
40c0bbcfb4 fix: assignee validation when updating issues (#6720)
* fix: assignee validation

* chore: remove prints

* fix: remove all assignees
2025-03-07 13:08:34 +05:30
Aaryan Khandelwal
7005ae2b53 chore: add more translation keys (#6715) 2025-03-07 11:31:41 +05:30
Vamsi Krishna
21d7a1865c fix: sidebar project list expand (#6714) 2025-03-06 16:54:28 +05:30
Aaryan Khandelwal
f65b9a4dcb improvement: add disable image upload using props (#6706) 2025-03-06 16:03:35 +05:30
Prateek Shourya
6d216f2607 [WEB-3482] refactor: platform components and mobx stores (#6713)
* improvement: platform componenents and mobx stores

* minor improvements
2025-03-06 15:47:46 +05:30
Vipin Chaudhary
4958be7898 fix: added padding bottom to editor container (#6712) 2025-03-06 15:38:53 +05:30
Vamsi Krishna
a40e44c6d5 refactor: issue list modal refactor (#6702) 2025-03-06 13:45:07 +05:30
Akshita Goyal
44af90dc6c fix: issue stats refactor (#6705)
* fix: issue stats refactor

* fix: refactor

* fix: ui color

* fix: translation key
2025-03-06 13:44:37 +05:30
223 changed files with 18584 additions and 1370 deletions

View File

@@ -38,3 +38,9 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"

View File

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

View File

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

View File

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

View File

@@ -59,4 +59,10 @@ APP_BASE_URL=
# Hard delete files after days
HARD_DELETE_AFTER_DAYS=60
HARD_DELETE_AFTER_DAYS=60
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"

View File

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

View File

@@ -1,9 +1,13 @@
# python imports
import os
# Third party imports
from rest_framework.throttling import SimpleRateThrottle
class ApiKeyRateThrottle(SimpleRateThrottle):
scope = "api_key"
rate = "60/minute"
rate = os.environ.get("API_KEY_RATE_LIMIT", "60/minute")
def get_cache_key(self, request, view):
# Retrieve the API key from the request header

View File

@@ -111,25 +111,23 @@ class IssueCreateSerializer(BaseSerializer):
data["label_ids"] = label_ids if label_ids else []
return data
def validate(self, data):
def validate(self, attrs):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
attrs.get("start_date", None) is not None
and attrs.get("target_date", None) is not None
and attrs.get("start_date", None) > attrs.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def get_valid_assignees(self, assignees, project_id):
if not assignees:
return []
if attrs.get("assignee_ids", []):
attrs["assignee_ids"] = ProjectMember.objects.filter(
project_id=self.context["project_id"],
role__gte=15,
is_active=True,
member_id__in=attrs["assignee_ids"],
).values_list("member_id", flat=True)
return ProjectMember.objects.filter(
project_id=project_id,
role__gte=15,
is_active=True,
member_id__in=assignees
).values_list('member_id', flat=True)
return attrs
def create(self, validated_data):
assignees = validated_data.pop("assignee_ids", None)
@@ -146,20 +144,19 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id
valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
if valid_assignee_ids is not None and len(valid_assignee_ids):
if assignees is not None and len(assignees):
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=user_id,
assignee_id=assignee_id,
issue=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user_id in valid_assignee_ids
for assignee_id in assignees
],
batch_size=10,
)
@@ -167,12 +164,15 @@ class IssueCreateSerializer(BaseSerializer):
pass
else:
# Then assign it to default assignee, if it is a valid assignee
if default_assignee_id is not None and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True
).exists():
if (
default_assignee_id is not None
and ProjectMember.objects.filter(
member_id=default_assignee_id,
project_id=project_id,
role__gte=15,
is_active=True,
).exists()
):
try:
IssueAssignee.objects.create(
assignee_id=default_assignee_id,
@@ -216,21 +216,20 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id
valid_assignee_ids = self.get_valid_assignees(assignees, project_id)
if valid_assignee_ids is not None:
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
try:
IssueAssignee.objects.bulk_create(
[
IssueAssignee(
assignee_id=user_id,
assignee_id=assignee_id,
issue=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for user_id in valid_assignee_ids
for assignee_id in assignees
],
batch_size=10,
ignore_conflicts=True,
@@ -269,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
source_data = serializers.SerializerMethodField()
def get_source_data(self, obj):
if (
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return {
"source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email,
"extra": obj.issue.source_data[0].extra,
}
return None
class Meta:
model = IssueActivity

View File

@@ -178,7 +178,9 @@ class IntakeIssueViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id
).first()
if not intake:
return Response({"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND)
return Response(
{"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND
)
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
@@ -385,7 +387,7 @@ class IntakeIssueViewSet(BaseViewSet):
}
issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True
issue, data=issue_data, partial=True, context={"project_id": project_id}
)
if issue_serializer.is_valid():

View File

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

View File

@@ -635,7 +635,9 @@ class IssueViewSet(BaseViewSet):
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
serializer = IssueCreateSerializer(
issue, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -1099,7 +1101,6 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
class IssueMetaEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="PROJECT")
def get(self, request, slug, project_id, issue_id):
issue = Issue.issue_objects.only("sequence_id", "project__identifier").get(
@@ -1115,14 +1116,12 @@ class IssueMetaEndpoint(BaseAPIView):
class IssueDetailIdentifierEndpoint(BaseAPIView):
def strict_str_to_int(self, s):
if not s.isdigit() and not (s.startswith('-') and s[1:].isdigit()):
if not s.isdigit() and not (s.startswith("-") and s[1:].isdigit()):
raise ValueError("Invalid integer string")
return int(s)
def get(self, request, slug, project_identifier, issue_identifier):
# Check if the issue identifier is a valid integer
try:
issue_identifier = self.strict_str_to_int(issue_identifier)
@@ -1134,8 +1133,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
# Fetch the project
project = Project.objects.get(
identifier__iexact=project_identifier,
workspace__slug=slug,
identifier__iexact=project_identifier, workspace__slug=slug
)
# Check if the user is a member of the project
@@ -1237,8 +1235,8 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
workspace__slug=slug,
project_id=project.id,
workspace__slug=slug,
project_id=project.id,
issue__sequence_id=issue_identifier,
subscriber=request.user,
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,12 @@ class S3Storage(S3Boto3Storage):
) or os.environ.get("MINIO_ENDPOINT_URL")
if os.environ.get("USE_MINIO") == "1":
# Determine protocol based on environment variable
if os.environ.get("MINIO_ENDPOINT_SSL") == "1":
endpoint_protocol = "https"
else:
endpoint_protocol = request.scheme if request else "http"
# Create an S3 client for MinIO
self.s3_client = boto3.client(
"s3",
@@ -39,7 +45,7 @@ class S3Storage(S3Boto3Storage):
aws_secret_access_key=self.aws_secret_access_key,
region_name=self.aws_region,
endpoint_url=(
f"{request.scheme}://{request.get_host()}"
f"{endpoint_protocol}://{request.get_host()}"
if request
else self.aws_s3_endpoint_url
),

View File

@@ -12,7 +12,7 @@ from rest_framework.response import Response
# Module imports
from .base import BaseViewSet
from plane.db.models import IntakeIssue, Issue, State, IssueLink, FileAsset, DeployBoard
from plane.db.models import IntakeIssue, Issue, IssueLink, FileAsset, DeployBoard
from plane.app.serializers import (
IssueSerializer,
IntakeIssueSerializer,
@@ -202,7 +202,12 @@ class IntakeIssuePublicViewSet(BaseViewSet):
"description": issue_data.get("description", issue.description),
}
issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True)
issue_serializer = IssueCreateSerializer(
issue,
data=issue_data,
partial=True,
context={"project_id": project_deploy_board.project_id},
)
if issue_serializer.is_valid():
current_instance = issue

View File

@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.18
Django==4.2.20
# rest framework
djangorestframework==3.15.2
# postgres

View File

@@ -50,6 +50,8 @@ x-app-env: &app-env
DATABASE_URL: ${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
SECRET_KEY: ${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
AMQP_URL: ${AMQP_URL:-amqp://plane:plane@plane-mq:5672/plane}
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
services:
web:

View File

@@ -58,3 +58,8 @@ GUNICORN_WORKERS=1
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64
# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"

View File

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

View File

@@ -2,7 +2,7 @@
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git",
"version": "0.25.1",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@@ -28,7 +28,9 @@
},
"resolutions": {
"nanoid": "3.3.8",
"esbuild": "0.25.0"
"esbuild": "0.25.0",
"@babel/helpers": "7.26.10",
"@babel/runtime": "7.26.10"
},
"packageManager": "yarn@1.22.22"
}

View File

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

View File

@@ -1,8 +1,4 @@
import {
TIssueGroupByOptions,
TIssueOrderByOptions,
IIssueDisplayProperties,
} from "@plane/types";
import { TIssueGroupByOptions, TIssueOrderByOptions, IIssueDisplayProperties } from "@plane/types";
export const ALL_ISSUES = "All Issues";
@@ -149,25 +145,24 @@ export const ISSUE_ORDER_BY_OPTIONS: {
{ key: "-priority", titleTranslationKey: "common.priority" },
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
[
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [
"assignee",
"start_date",
"due_date",
"labels",
"key",
"priority",
"state",
"sub_issue_count",
"link",
"attachment_count",
"estimate",
"created_on",
"updated_on",
"modules",
"cycle",
"issue_type",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
@@ -215,3 +210,144 @@ export const ISSUE_DISPLAY_PROPERTIES: {
{ key: "modules", titleTranslationKey: "common.module" },
{ key: "cycle", titleTranslationKey: "common.cycle" },
];
export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
"state",
"priority",
"assignee",
"labels",
"modules",
"cycle",
"start_date",
"due_date",
"estimate",
"created_on",
"updated_on",
"link",
"attachment_count",
"sub_issue_count",
];
export const SPREADSHEET_PROPERTY_DETAILS: {
[key in keyof IIssueDisplayProperties]: {
i18n_title: string;
ascendingOrderKey: TIssueOrderByOptions;
ascendingOrderTitle: string;
descendingOrderKey: TIssueOrderByOptions;
descendingOrderTitle: string;
icon: string;
};
} = {
assignee: {
i18n_title: "common.assignees",
ascendingOrderKey: "assignees__first_name",
ascendingOrderTitle: "A",
descendingOrderKey: "-assignees__first_name",
descendingOrderTitle: "Z",
icon: "Users",
},
created_on: {
i18n_title: "common.sort.created_on",
ascendingOrderKey: "-created_at",
ascendingOrderTitle: "New",
descendingOrderKey: "created_at",
descendingOrderTitle: "Old",
icon: "CalendarDays",
},
due_date: {
i18n_title: "common.order_by.due_date",
ascendingOrderKey: "-target_date",
ascendingOrderTitle: "New",
descendingOrderKey: "target_date",
descendingOrderTitle: "Old",
icon: "CalendarCheck2",
},
estimate: {
i18n_title: "common.estimate",
ascendingOrderKey: "estimate_point__key",
ascendingOrderTitle: "Low",
descendingOrderKey: "-estimate_point__key",
descendingOrderTitle: "High",
icon: "Triangle",
},
labels: {
i18n_title: "common.labels",
ascendingOrderKey: "labels__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-labels__name",
descendingOrderTitle: "Z",
icon: "Tag",
},
modules: {
i18n_title: "common.modules",
ascendingOrderKey: "issue_module__module__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-issue_module__module__name",
descendingOrderTitle: "Z",
icon: "DiceIcon",
},
cycle: {
i18n_title: "common.cycle",
ascendingOrderKey: "issue_cycle__cycle__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-issue_cycle__cycle__name",
descendingOrderTitle: "Z",
icon: "ContrastIcon",
},
priority: {
i18n_title: "common.priority",
ascendingOrderKey: "priority",
ascendingOrderTitle: "None",
descendingOrderKey: "-priority",
descendingOrderTitle: "Urgent",
icon: "Signal",
},
start_date: {
i18n_title: "common.order_by.start_date",
ascendingOrderKey: "-start_date",
ascendingOrderTitle: "New",
descendingOrderKey: "start_date",
descendingOrderTitle: "Old",
icon: "CalendarClock",
},
state: {
i18n_title: "common.state",
ascendingOrderKey: "state__name",
ascendingOrderTitle: "A",
descendingOrderKey: "-state__name",
descendingOrderTitle: "Z",
icon: "DoubleCircleIcon",
},
updated_on: {
i18n_title: "common.sort.updated_on",
ascendingOrderKey: "-updated_at",
ascendingOrderTitle: "New",
descendingOrderKey: "updated_at",
descendingOrderTitle: "Old",
icon: "CalendarDays",
},
link: {
i18n_title: "common.link",
ascendingOrderKey: "-link_count",
ascendingOrderTitle: "Most",
descendingOrderKey: "link_count",
descendingOrderTitle: "Least",
icon: "Link2",
},
attachment_count: {
i18n_title: "common.attachment",
ascendingOrderKey: "-attachment_count",
ascendingOrderTitle: "Most",
descendingOrderKey: "attachment_count",
descendingOrderTitle: "Least",
icon: "Paperclip",
},
sub_issue_count: {
i18n_title: "issue.display.properties.sub_issue",
ascendingOrderKey: "-sub_issues_count",
ascendingOrderTitle: "Most",
descendingOrderKey: "sub_issues_count",
descendingOrderTitle: "Least",
icon: "LayersIcon",
},
};

View File

@@ -1,3 +1,4 @@
export * from "./common";
export * from "./filter";
export * from "./layout";
export * from "./modal";

View File

@@ -0,0 +1,19 @@
// plane imports
import { TIssue } from "@plane/types";
export const DEFAULT_WORK_ITEM_FORM_VALUES: Partial<TIssue> = {
project_id: "",
type_id: null,
name: "",
description_html: "",
estimate_point: null,
state_id: "",
parent_id: null,
priority: "none",
assignee_ids: [],
label_ids: [],
cycle_id: null,
module_ids: null,
start_date: null,
target_date: null,
};

View File

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

View File

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

View File

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

View File

@@ -76,7 +76,7 @@ export const CustomImageNode = (props: CustomImageNodeProps) => {
failedToLoadImage={failedToLoadImage}
getPos={getPos}
loadImageFromFileSystem={setImageFromFileSystem}
maxFileSize={editor.storage.imageComponent.maxFileSize}
maxFileSize={editor.storage.imageComponent?.maxFileSize}
node={node}
setIsUploaded={setIsUploaded}
selected={selected}

View File

@@ -16,7 +16,7 @@ export const ImageUploadStatus: React.FC<Props> = (props) => {
// subscribe to image upload status
const uploadStatus: number | undefined = useEditorState({
editor,
selector: ({ editor }) => editor.storage.imageComponent.assetsUploadStatus[nodeId],
selector: ({ editor }) => editor.storage.imageComponent?.assetsUploadStatus[nodeId],
});
useEffect(() => {

View File

@@ -22,7 +22,7 @@ declare module "@tiptap/core" {
imageComponent: {
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
uploadImage: (blockId: string, file: File) => () => Promise<string> | undefined;
updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
updateAssetsUploadStatus?: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void;
getImageSource?: (path: string) => () => Promise<string>;
restoreImage: (src: string) => () => Promise<void>;
};

View File

@@ -50,8 +50,7 @@ type TArguments = {
export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args;
return [
// @ts-expect-error tiptap types are incorrect
const extensions = [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -109,12 +108,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
},
}),
CustomTypographyExtension,
ImageExtension(fileHandler).configure({
HTMLAttributes: {
class: "rounded-md",
},
}),
CustomImageExtension(fileHandler),
TiptapUnderline,
TextStyle,
TaskList.configure({
@@ -152,7 +145,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
if (editor.storage.imageComponent.uploadInProgress) return "";
if (editor.storage.imageComponent?.uploadInProgress) return "";
const shouldHidePlaceholder =
editor.isActive("table") ||
@@ -179,4 +172,18 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
disabledExtensions,
}),
];
if (!disabledExtensions.includes("image")) {
extensions.push(
ImageExtension(fileHandler).configure({
HTMLAttributes: {
class: "rounded-md",
},
}),
CustomImageExtension(fileHandler)
);
}
// @ts-expect-error tiptap types are incorrect
return extensions;
};

View File

@@ -48,6 +48,7 @@ export const CustomImageComponentWithoutProps = () =>
return {
fileMap: new Map(),
deletedImageSet: new Map<string, boolean>(),
assetsUploadStatus: {},
};
},
});

View File

@@ -41,8 +41,7 @@ type Props = {
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
const { disabledExtensions, fileHandler, mentionHandler } = props;
return [
// @ts-expect-error tiptap types are incorrect
const extensions = [
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -94,12 +93,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
},
}),
CustomTypographyExtension,
ReadOnlyImageExtension(fileHandler).configure({
HTMLAttributes: {
class: "rounded-md",
},
}),
CustomReadOnlyImageExtension(fileHandler),
TiptapUnderline,
TextStyle,
TaskList.configure({
@@ -136,4 +129,18 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
disabledExtensions,
}),
];
if (!disabledExtensions.includes("image")) {
extensions.push(
ReadOnlyImageExtension(fileHandler).configure({
HTMLAttributes: {
class: "rounded-md",
},
}),
CustomReadOnlyImageExtension(fileHandler)
);
}
// @ts-expect-error tiptap types are incorrect
return extensions;
};

View File

@@ -43,7 +43,7 @@ import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/typ
// plane editor extensions
import { coreEditorAdditionalSlashCommandOptions } from "@/plane-editor/extensions";
// local types
import { TExtensionProps } from "./root";
import { TExtensionProps, TSlashCommandAdditionalOption } from "./root";
export type TSlashCommandSection = {
key: TSlashCommandSectionKeys;
@@ -54,7 +54,7 @@ export type TSlashCommandSection = {
export const getSlashCommandFilteredSections =
(args: TExtensionProps) =>
({ query }: { query: string }): TSlashCommandSection[] => {
const { additionalOptions, disabledExtensions } = args;
const { additionalOptions: externalAdditionalOptions, disabledExtensions } = args;
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
{
key: "general",
@@ -176,15 +176,6 @@ export const getSlashCommandFilteredSections =
icon: <Code2 className="size-3.5" />,
command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
commandKey: "image",
key: "image",
title: "Image",
icon: <ImageIcon className="size-3.5" />,
description: "Insert an image",
searchTerms: ["img", "photo", "picture", "media", "upload"],
command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }),
},
{
commandKey: "callout",
key: "callout",
@@ -284,8 +275,24 @@ export const getSlashCommandFilteredSections =
},
];
const internalAdditionalOptions: TSlashCommandAdditionalOption[] = [];
if (!disabledExtensions?.includes("image")) {
internalAdditionalOptions.push({
commandKey: "image",
key: "image",
title: "Image",
icon: <ImageIcon className="size-3.5" />,
description: "Insert an image",
searchTerms: ["img", "photo", "picture", "media", "upload"],
command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }),
section: "general",
pushAfter: "code",
});
}
[
...(additionalOptions ?? []),
...internalAdditionalOptions,
...(externalAdditionalOptions ?? []),
...coreEditorAdditionalSlashCommandOptions({
disabledExtensions,
}),

View File

@@ -111,7 +111,7 @@ export const useEditor = (props: CustomEditorProps) => {
// value is null when intentionally passed where syncing is not yet
// supported and value is undefined when the data from swr is not populated
if (value == null) return;
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
if (editor && !editor.isDestroyed && !editor.storage.imageComponent?.uploadInProgress) {
try {
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
if (editor.state.selection) {
@@ -129,7 +129,7 @@ export const useEditor = (props: CustomEditorProps) => {
useEffect(() => {
if (!editor) return;
const assetsUploadStatus = fileHandler.assetsUploadStatus;
editor.commands.updateAssetsUploadStatus(assetsUploadStatus);
editor.commands.updateAssetsUploadStatus?.(assetsUploadStatus);
}, [editor, fileHandler.assetsUploadStatus]);
useImperativeHandle(
@@ -221,7 +221,7 @@ export const useEditor = (props: CustomEditorProps) => {
if (!editor) return;
scrollSummary(editor, marking);
},
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
isEditorReadyToDiscard: () => editor?.storage.imageComponent?.uploadInProgress === false,
setFocusAtPosition: (position: number) => {
if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed.");

View File

@@ -21,7 +21,9 @@ export const useUploader = (args: TUploaderArgs) => {
const uploadFile = useCallback(
async (file: File) => {
const setImageUploadInProgress = (isUploading: boolean) => {
editor.storage.imageComponent.uploadInProgress = isUploading;
if (editor.storage.imageComponent) {
editor.storage.imageComponent.uploadInProgress = isUploading;
}
};
setImageUploadInProgress(true);
setUploading(true);

View File

@@ -1 +1 @@
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key";
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands" | "enter-key" | "image";

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -529,6 +529,7 @@
"property": "Property",
"properties": "Properties",
"parent": "Parent",
"page": "Page",
"remove": "Remove",
"archiving": "Archiving",
"archive": "Archive",
@@ -687,7 +688,7 @@
"you": "You",
"upgrade_cta": {
"higher_subscription": "Upgrade to higher subscription",
"talk_to_sales": "Talk to sales"
"talk_to_sales": "Talk to Sales"
},
"category": "Category",
"categories": "Categories",
@@ -696,7 +697,8 @@
"delete": "Delete",
"deleting": "Deleting",
"pending": "Pending",
"invite": "Invite"
"invite": "Invite",
"view": "View"
},
"chart": {
@@ -814,7 +816,8 @@
"sub_issue_count": "Sub-work item count",
"attachment_count": "Attachment count",
"created_on": "Created on",
"sub_issue": "Sub-work item"
"sub_issue": "Sub-work item",
"work_item_count": "Work item count"
},
"extra": {
"show_sub_issues": "Show sub-work items",
@@ -1302,7 +1305,8 @@
"max_length": "Workspace name should not exceed 80 characters"
},
"company_size": {
"required": "Company size is required"
"required": "Company size is required",
"select_a_range": "Select organization size"
}
}
},

View File

@@ -701,6 +701,7 @@
"property": "Propiedad",
"properties": "Propiedades",
"parent": "Padre",
"page": "página",
"remove": "Eliminar",
"archiving": "Archivando",
"archive": "Archivar",
@@ -867,7 +868,8 @@
"delete": "Eliminar",
"deleting": "Eliminando",
"pending": "Pendiente",
"invite": "Invitar"
"invite": "Invitar",
"view": "Ver"
},
"chart": {
@@ -985,7 +987,8 @@
"sub_issue_count": "Cantidad de sub-elementos",
"attachment_count": "Cantidad de archivos adjuntos",
"created_on": "Creado el",
"sub_issue": "Sub-elemento de trabajo"
"sub_issue": "Sub-elemento de trabajo",
"work_item_count": "Recuento de elementos de trabajo"
},
"extra": {
"show_sub_issues": "Mostrar sub-elementos",
@@ -1472,7 +1475,8 @@
"max_length": "El nombre del espacio de trabajo no debe exceder los 80 caracteres"
},
"company_size": {
"required": "El tamaño de la empresa es obligatorio"
"required": "El tamaño de la empresa es obligatorio",
"select_a_range": "Seleccionar tamaño de la organización"
}
}
},

View File

@@ -699,6 +699,7 @@
"property": "Propriété",
"properties": "Propriétés",
"parent": "Parent",
"page": "Pâge",
"remove": "Supprimer",
"archiving": "Archivage",
"archive": "Archiver",
@@ -856,7 +857,7 @@
"you": "Vous",
"upgrade_cta": {
"higher_subscription": "Passer à une abonnement plus élevé",
"talk_to_sales": "Parler à la vente"
"talk_to_sales": "Parler aux ventes"
},
"category": "Catégorie",
"categories": "Catégories",
@@ -865,7 +866,8 @@
"delete": "Supprimer",
"deleting": "Suppression",
"pending": "En attente",
"invite": "Inviter"
"invite": "Inviter",
"view": "Afficher"
},
"chart": {
@@ -983,7 +985,8 @@
"sub_issue_count": "Nombre de sous-éléments",
"attachment_count": "Nombre de pièces jointes",
"created_on": "Créé le",
"sub_issue": "Sous-élément de travail"
"sub_issue": "Sous-élément de travail",
"work_item_count": "Nombre d'éléments de travail"
},
"extra": {
"show_sub_issues": "Afficher les sous-éléments",
@@ -1470,7 +1473,8 @@
"max_length": "Le nom de l'espace de travail ne doit pas dépasser 80 caractères"
},
"company_size": {
"required": "La taille de l'entreprise est requise"
"required": "La taille de l'entreprise est requise",
"select_a_range": "Sélectionner la taille de l'organisation"
}
}
},

View File

@@ -693,6 +693,7 @@
"property": "Proprietà",
"properties": "Proprietà",
"parent": "Principale",
"page": "Pagina",
"remove": "Rimuovi",
"archiving": "Archiviazione in corso",
"archive": "Archivia",
@@ -862,7 +863,8 @@
"delete": "Elimina",
"deleting": "Eliminazione in corso",
"pending": "In sospeso",
"invite": "Invita"
"invite": "Invita",
"view": "Visualizza"
},
"chart": {
@@ -980,7 +982,8 @@
"sub_issue_count": "Numero di sotto-elementi di lavoro",
"attachment_count": "Numero di allegati",
"created_on": "Creato il",
"sub_issue": "Sotto-elemento di lavoro"
"sub_issue": "Sotto-elemento di lavoro",
"work_item_count": "Conteggio degli elementi di lavoro"
},
"extra": {
"show_sub_issues": "Mostra sotto-elementi di lavoro",
@@ -1468,7 +1471,8 @@
"max_length": "Il nome dello spazio di lavoro non deve superare gli 80 caratteri"
},
"company_size": {
"required": "La dimensione aziendale è obbligatoria"
"required": "La dimensione aziendale è obbligatoria",
"select_a_range": "Seleziona la dimensione dell'organizzazione"
}
}
},

View File

@@ -699,6 +699,7 @@
"property": "プロパティ",
"properties": "プロパティ",
"parent": "親",
"page": "ページ",
"remove": "削除",
"archiving": "アーカイブ中",
"archive": "アーカイブ",
@@ -856,7 +857,7 @@
"you": "あなた",
"upgrade_cta": {
"higher_subscription": "高いサブスクリプションにアップグレード",
"talk_to_sales": "セールスに連絡"
"talk_to_sales": "トーク トゥ セールス"
},
"category": "カテゴリー",
"categories": "カテゴリーズ",
@@ -865,7 +866,8 @@
"delete": "デリート",
"deleting": "デリーティング",
"pending": "保留中",
"invite": "招待"
"invite": "招待",
"view": "ビュー"
},
"chart": {
@@ -983,7 +985,8 @@
"sub_issue_count": "サブ作業項目数",
"attachment_count": "添付ファイル数",
"created_on": "作成日",
"sub_issue": "サブ作業項目"
"sub_issue": "サブ作業項目",
"work_item_count": "作業項目数"
},
"extra": {
"show_sub_issues": "サブ作業項目を表示",
@@ -1470,7 +1473,8 @@
"max_length": "ワークスペース名は80文字を超えることはできません"
},
"company_size": {
"required": "会社の規模は必須です"
"required": "会社の規模は必須です",
"select_a_range": "組織の規模を選択"
}
}
},

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -697,6 +697,7 @@
"property": "Свойство",
"properties": "Свойства",
"parent": "Родительский",
"page": "Пейдж",
"remove": "Удалить",
"archiving": "Архивация",
"archive": "Архивировать",
@@ -864,7 +865,8 @@
"delete": "Удалить",
"deleting": "Удаление",
"pending": "Ожидание",
"invite": "Пригласить"
"invite": "Пригласить",
"view": "Просмотр"
},
"chart": {
@@ -982,7 +984,8 @@
"sub_issue_count": "Количество подэлементов",
"attachment_count": "Количество вложений",
"created_on": "Дата создания",
"sub_issue": "Подэлемент"
"sub_issue": "Подэлемент",
"work_item_count": "Количество рабочих элементов"
},
"extra": {
"show_sub_issues": "Показывать подэлементы",
@@ -1470,7 +1473,8 @@
"max_length": "Максимум 80 символов"
},
"company_size": {
"required": "Размер компании обязателен"
"required": "Размер компании обязателен",
"select_a_range": "Выберите размер организации"
}
}
},
@@ -1866,7 +1870,7 @@
}
},
"completed_no_issues": {
"title": "Нет рабочих элементов в цикле",
"title": "Нет рабочих элементов в цикле",
"description": "Нет рабочих элементов. Рабочие элементы были перенесены или скрыты. Для просмотра измените настройки отображения."
},
"active": {
@@ -2285,7 +2289,7 @@
"short_description": "Экспорт в csv"
},
"excel": {
"title": "Excel",
"title": "Excel",
"description": "Экспорт рабочих элементов в файл Excel.",
"short_description": "Экспорт в excel"
},
@@ -2303,7 +2307,7 @@
"default_global_view": {
"all_issues": "Все рабочие элементы",
"assigned": "Назначенные",
"created": "Созданные",
"created": "Созданные",
"subscribed": "Подписанные"
},
@@ -2332,7 +2336,7 @@
"project_modules": {
"status": {
"backlog": "Бэклог",
"planned": "Запланировано",
"planned": "Запланировано",
"in_progress": "В процессе",
"paused": "Приостановлено",
"completed": "Завершено",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -699,6 +699,7 @@
"property": "属性",
"properties": "属性",
"parent": "父项",
"page": "页面",
"remove": "移除",
"archiving": "归档中",
"archive": "归档",
@@ -865,7 +866,8 @@
"delete": "删除",
"deleting": "删除中",
"pending": "待处理",
"invite": "邀请"
"invite": "邀请",
"view": "查看"
},
"chart": {
@@ -983,7 +985,8 @@
"sub_issue_count": "子工作项数量",
"attachment_count": "附件数量",
"created_on": "创建于",
"sub_issue": "子工作项"
"sub_issue": "子工作项",
"work_item_count": "工作项数量"
},
"extra": {
"show_sub_issues": "显示子工作项",
@@ -1470,7 +1473,8 @@
"max_length": "工作区名称不应超过80个字符"
},
"company_size": {
"required": "公司规模为必填项"
"required": "公司规模为必填项",
"select_a_range": "选择组织规模"
}
}
},

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,3 +40,4 @@ export * from "./epics";
export * from "./charts";
export * from "./home";
export * from "./stickies";
export * from "./utils";

View File

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

View File

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

7
packages/types/src/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export type PartialDeep<K> = {
[attr in keyof K]?: K[attr] extends object ? PartialDeep<K[attr]> : K[attr];
};
export type CompleteOrEmpty<T> = T | Record<string, never>;
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

View File

@@ -1,9 +1,6 @@
export type TIssueLayouts =
| "list"
| "kanban"
| "calendar"
| "spreadsheet"
| "gantt_chart";
import { TIssue } from "./issues/issue";
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
export type TIssueGroupByOptions =
| "state"
@@ -211,3 +208,10 @@ export interface IssuePaginationOptions {
subGroupedBy?: TIssueGroupByOptions;
orderBy?: TIssueOrderByOptions;
}
export type TSpreadsheetColumn = React.FC<{
issue: TIssue;
onClose: () => void;
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
disabled: boolean;
}>;

View File

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

View File

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

View File

@@ -8,71 +8,76 @@ import { cn } from "../helpers";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
// classNames={{
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
// month: "space-y-4",
// // caption: "flex justify-center pt-1 relative items-center",
// // caption_label: "hidden",
// nav: "box-border absolute top-[1.2rem] right-[1rem] flex items-center",
// button_next:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// button_previous:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// chevron: "m-0 ml-1 size-[0.75rem]",
// // nav_button: cn("h-10 bg-transparent p-0 opacity-50 hover:opacity-100"),
// // nav_button_previous: "absolute left-1",
// // nav_button_next: "absolute right-1",
// table: "w-full border-collapse space-y-1",
// head_row: "flex w-full items-center",
// head_cell: "rounded-md w-10 text-[10px] text-center m-auto font-semibold uppercase",
// row: "flex w-full mt-2",
// cell: cn(
// "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-custom-primary-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-full",
// props.mode === "range"
// ? "[&:has(>.day-range-end)]:rounded-r-full [&:has(>.day-range-start)]:rounded-l-full first:[&:has([aria-selected])]:rounded-l-full last:[&:has([aria-selected])]:rounded-r-full"
// : "[&:has([aria-selected])]:rounded-full [&:has([aria-selected])]:bg-custom-primary-100 [&:has([aria-selected])]:text-white"
// ),
// // day_button:
// // "size-10 flex items-center justify-center overflow-hidden box-border m-0 border-2 border-transparent rounded-full",
// day: "size-10 p-0 font-normal aria-selected:opacity-100 rounded-full hover:bg-custom-primary-100/60",
// day_range_start: "day-range-start bg-custom-primary-100 text-white",
// day_range_end: "day-range-end bg-custom-primary-100 text-white",
// day_selected: "",
// day_today:
// "relative after:content-[''] after:absolute after:m-auto after:left-1/3 after:bottom-[6px] after:w-[6px] after:h-[6px] after:bg-custom-primary-100/50 after:rounded-full after:translate-x-1/2 after:translate-y-1/2",
// day_outside: "day-outside",
// day_disabled: "opacity-50 hover:!bg-transparent",
// day_range_middle: "text-black",
// day_hidden: "invisible",
// caption_dropdowns: "inline-flex bg-transparent",
// dropdown_root: "m-0 relative inline-flex items-center",
// dropdowns: "relative inline-flex items-center",
// dropdown:
// "appearance-none absolute z-[2] top-0 bottom-0 left-0 w-full m-0 p-0 opacity-0 border-none text-[1rem] cursor-pointer bg-transparent hover:bg-custom-background-80",
// months_dropdown: "capitalize",
// caption_label:
// "z-[1] inline-flex items-center gap-[0.25rem] m-0 py-0 px-[0.25rem] whitespace-nowrap border-2 border-transparent font-semibold bg-transparent rounded",
// ...classNames,
// }}
components={{
Chevron: ({ className, ...props }) => (
<ChevronLeft
className={cn(
"size-4",
{
"rotate-180": props.orientation === "right",
"-rotate-90": props.orientation === "down",
},
className
)}
{...props}
/>
),
}}
{...props}
/>
);
export const Calendar = ({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) => {
const currentYear = new Date().getFullYear();
const thirtyYearsAgoFirstDay = new Date(currentYear - 30, 0, 1);
const thirtyYearsFromNowFirstDay = new Date(currentYear + 30, 11, 31);
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
// classNames={{
// months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
// month: "space-y-4",
// // caption: "flex justify-center pt-1 relative items-center",
// // caption_label: "hidden",
// nav: "box-border absolute top-[1.2rem] right-[1rem] flex items-center",
// button_next:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// button_previous:
// "size-[1.25rem] border-none bg-none p-[0.25rem] m-0 cursor-pointer inline-flex items-center justify-center relative appearance-none rounded-sm hover:bg-custom-background-80 focus-visible:bg-custom-background-80",
// chevron: "m-0 ml-1 size-[0.75rem]",
// // nav_button: cn("h-10 bg-transparent p-0 opacity-50 hover:opacity-100"),
// // nav_button_previous: "absolute left-1",
// // nav_button_next: "absolute right-1",
// table: "w-full border-collapse space-y-1",
// head_row: "flex w-full items-center",
// head_cell: "rounded-md w-10 text-[10px] text-center m-auto font-semibold uppercase",
// row: "flex w-full mt-2",
// cell: cn(
// "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-custom-primary-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-full",
// props.mode === "range"
// ? "[&:has(>.day-range-end)]:rounded-r-full [&:has(>.day-range-start)]:rounded-l-full first:[&:has([aria-selected])]:rounded-l-full last:[&:has([aria-selected])]:rounded-r-full"
// : "[&:has([aria-selected])]:rounded-full [&:has([aria-selected])]:bg-custom-primary-100 [&:has([aria-selected])]:text-white"
// ),
// // day_button:
// // "size-10 flex items-center justify-center overflow-hidden box-border m-0 border-2 border-transparent rounded-full",
// day: "size-10 p-0 font-normal aria-selected:opacity-100 rounded-full hover:bg-custom-primary-100/60",
// day_range_start: "day-range-start bg-custom-primary-100 text-white",
// day_range_end: "day-range-end bg-custom-primary-100 text-white",
// day_selected: "",
// day_today:
// "relative after:content-[''] after:absolute after:m-auto after:left-1/3 after:bottom-[6px] after:w-[6px] after:h-[6px] after:bg-custom-primary-100/50 after:rounded-full after:translate-x-1/2 after:translate-y-1/2",
// day_outside: "day-outside",
// day_disabled: "opacity-50 hover:!bg-transparent",
// day_range_middle: "text-black",
// day_hidden: "invisible",
// caption_dropdowns: "inline-flex bg-transparent",
// dropdown_root: "m-0 relative inline-flex items-center",
// dropdowns: "relative inline-flex items-center",
// dropdown:
// "appearance-none absolute z-[2] top-0 bottom-0 left-0 w-full m-0 p-0 opacity-0 border-none text-[1rem] cursor-pointer bg-transparent hover:bg-custom-background-80",
// months_dropdown: "capitalize",
// caption_label:
// "z-[1] inline-flex items-center gap-[0.25rem] m-0 py-0 px-[0.25rem] whitespace-nowrap border-2 border-transparent font-semibold bg-transparent rounded",
// ...classNames,
// }}
components={{
Chevron: ({ className, ...props }) => (
<ChevronLeft
className={cn(
"size-4",
{ "rotate-180": props.orientation === "right", "-rotate-90": props.orientation === "down" },
className
)}
{...props}
/>
),
}}
startMonth={thirtyYearsAgoFirstDay}
endMonth={thirtyYearsFromNowFirstDay}
{...props}
/>
);
};

View File

@@ -35,7 +35,7 @@ export const ScrollArea: FC<TScrollAreaProps> = (props) => {
<RadixScrollArea.Viewport className="size-full">{children}</RadixScrollArea.Viewport>
<RadixScrollArea.Scrollbar
className={cn(
"group/track flex touch-none select-none bg-transparent transition-colors duration-[160ms] ease-out",
"group/track flex touch-none select-none bg-transparent transition-colors duration-150 ease-out",
sizeStyles[size]
)}
orientation="vertical"
@@ -49,7 +49,7 @@ export const ScrollArea: FC<TScrollAreaProps> = (props) => {
</RadixScrollArea.Scrollbar>
<RadixScrollArea.Scrollbar
className={cn(
"group/track flex touch-none select-none bg-transparent transition-colors duration-[160ms] ease-out",
"group/track flex touch-none select-none bg-transparent transition-colors duration-150 ease-out",
sizeStyles[size]
)}
orientation="horizontal"

View File

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

View File

@@ -5,3 +5,37 @@ import { twMerge } from "tailwind-merge";
export const getSupportEmail = (defaultEmail: string = ""): string => defaultEmail;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
/**
* Extracts IDs from an array of objects with ID property
*/
export const extractIds = <T extends { id: string }>(items: T[]): string[] => items.map((item) => item.id);
/**
* Checks if an ID exists and is valid within the provided list
*/
export const isValidId = (id: string | null | undefined, validIds: string[]): boolean => !!id && validIds.includes(id);
/**
* Filters an array to only include valid IDs
*/
export const filterValidIds = (ids: string[], validIds: string[]): string[] =>
ids.filter((id) => validIds.includes(id));
/**
* Filters an array to include only valid IDs, returning both valid and invalid IDs
*/
export const partitionValidIds = (ids: string[], validIds: string[]): { valid: string[]; invalid: string[] } => {
const valid: string[] = [];
const invalid: string[] = [];
ids.forEach((id) => {
if (validIds.includes(id)) {
valid.push(id);
} else {
invalid.push(id);
}
});
return { valid, invalid };
};

View File

@@ -11,3 +11,4 @@ export * from "./state";
export * from "./string";
export * from "./theme";
export * from "./workspace";
export * from "./work-item";

View File

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

View File

@@ -0,0 +1,33 @@
// plane imports
import { DEFAULT_WORK_ITEM_FORM_VALUES } from "@plane/constants";
import { IPartialProject, ISearchIssueResponse, IState, TIssue } from "@plane/types";
export const getUpdateFormDataForReset = (projectId: string | null | undefined, formData: Partial<TIssue>) => ({
...DEFAULT_WORK_ITEM_FORM_VALUES,
project_id: projectId,
name: formData.name,
description_html: formData.description_html,
priority: formData.priority,
start_date: formData.start_date,
target_date: formData.target_date,
});
export const convertWorkItemDataToSearchResponse = (
workspaceSlug: string,
workItem: TIssue,
project: IPartialProject | undefined,
state: IState | undefined
): ISearchIssueResponse => ({
id: workItem.id,
name: workItem.name,
project_id: workItem.project_id ?? "",
project__identifier: project?.identifier ?? "",
project__name: project?.name ?? "",
sequence_id: workItem.sequence_id,
type_id: workItem.type_id ?? "",
state__color: state?.color ?? "",
start_date: workItem.start_date,
state__group: state?.group ?? "backlog",
state__name: state?.name ?? "",
workspace__slug: workspaceSlug,
});

View File

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

View File

@@ -1,6 +1,7 @@
import React from "react";
// editor
// plane imports
import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot, IssueCommentToolbar } from "@/components/editor";
// helpers
@@ -9,7 +10,7 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
import { isCommentEmpty } from "@/helpers/string.helper";
interface LiteTextEditorWrapperProps
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
extends MakeOptional<Omit<ILiteTextEditor, "fileHandler" | "mentionHandler">, "disabledExtensions"> {
anchor: string;
workspaceId: string;
isSubmitting?: boolean;
@@ -25,6 +26,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
isSubmitting = false,
showSubmitButton = true,
uploadFile,
disabledExtensions,
...rest
} = props;
function isMutableRefObject<T>(ref: React.ForwardedRef<T>): ref is React.MutableRefObject<T | null> {
@@ -38,7 +40,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
<div className="border border-custom-border-200 rounded p-3 space-y-3">
<LiteTextEditorWithRef
ref={ref}
disabledExtensions={[]}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getEditorFileHandlers({
anchor,
uploadFile,

View File

@@ -1,25 +1,26 @@
import React from "react";
// editor
// plane imports
import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
type LiteTextReadOnlyEditorWrapperProps = Omit<
ILiteTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, ...props }, ref) => (
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,

View File

@@ -1,20 +1,21 @@
import React, { forwardRef } from "react";
// editor
// plane imports
import { EditorRefApi, IRichTextEditor, RichTextEditorWithRef, TFileHandler } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
interface RichTextEditorWrapperProps
extends Omit<IRichTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
extends MakeOptional<Omit<IRichTextEditor, "fileHandler" | "mentionHandler">, "disabledExtensions"> {
anchor: string;
uploadFile: TFileHandler["upload"];
workspaceId: string;
}
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { anchor, containerClassName, uploadFile, workspaceId, ...rest } = props;
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
return (
<RichTextEditorWithRef
@@ -22,7 +23,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
ref={ref}
disabledExtensions={[]}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getEditorFileHandlers({
anchor,
uploadFile,
@@ -30,7 +31,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
})}
{...rest}
containerClassName={containerClassName}
editorClassName="min-h-[100px] max-h-[50vh] border border-gray-100 rounded-md pl-3 pb-3 overflow-y-scroll"
editorClassName="min-h-[100px] max-h-[50vh] border-[0.5px] border-custom-border-200 rounded-md pl-3 py-2 overflow-y-scroll"
/>
);
});

View File

@@ -1,25 +1,26 @@
import React from "react";
// editor
// plane imports
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWithRef } from "@plane/editor";
import { MakeOptional } from "@plane/types";
// components
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
type RichTextReadOnlyEditorWrapperProps = Omit<
IRichTextReadOnlyEditor,
"disabledExtensions" | "fileHandler" | "mentionHandler"
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
"disabledExtensions"
> & {
anchor: string;
workspaceId: string;
};
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, ...props }, ref) => (
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={[]}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,

View File

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

View File

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

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

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

View File

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

View File

@@ -4,7 +4,7 @@ import { FC, ReactNode } from "react";
// components
import { AppHeader } from "@/components/core";
// local components
import { ProjectSettingHeader } from "./header";
import { ProjectSettingHeader } from "../header";
import { ProjectSettingsSidebar } from "./sidebar";
export interface IProjectSettingLayout {

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