Compare commits

..
Author SHA1 Message Date
Vamsi krishna 750364833b feat: added user timezone dates for cycle 2025-03-26 12:39:42 +05:30
41447e566a [WEB-3600] fix: private project join issue (#6799)
* fix: private project join issue

* chore: return network value

* fix: refactor

* fix: refactor

* fix: type

* chore: added restricition for private projects

* chore: removed extra validations

* chore: added value to access enum

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-03-25 20:17:16 +05:30
Prateek ShouryaandGitHub cebd0b3599 [RANTS-68] fix: z-index for image picker popover button (change project cover) (#6812) 2025-03-25 20:16:06 +05:30
Prateek ShouryaandGitHub caa522118d [RANTS-29] fix: enter key does not work in the workspace member invite modal (#6816) 2025-03-25 20:15:35 +05:30
Prateek ShouryaandGitHub aea5f39059 [RANTS-31] improvement: optimistic update for home widget reordering (#6817) 2025-03-25 20:15:00 +05:30
sriram veeraghanta f29867968a chore: removed sentry instrumentation dependencies 2025-03-25 15:54:44 +05:30
sriram veeraghanta c91972cc0a chore: removing sentry instrumentation 2025-03-25 15:45:31 +05:30
Anmol Singh BhatiaandGitHub 84c7375d2a [WEB-3601] chore: content updated (#6811) 2025-03-24 19:57:13 +05:30
Anmol Singh BhatiaandGitHub 5cb37a0b9c [WEB-3560] fix: table layout issue block and code refactor (#6805) 2025-03-24 19:06:36 +05:30
Anmol Singh BhatiaandGitHub c347dd7dcd [WEB-3614] chore: list layout display filters (#6801) 2025-03-24 19:05:53 +05:30
sriram veeraghanta 0ec206b75d fix: transpile packages update on space and admin apps 2025-03-24 18:55:59 +05:30
Vamsi KrishnaandGitHub e8718a84fe chore: issue detail refactor (#6803) 2025-03-24 18:33:22 +05:30
Anmol Singh BhatiaandGitHub 983e0fa081 [WEB-3438] fix: transfer completed cycle issue modal (#6802) 2025-03-24 18:30:31 +05:30
Aaryan KhandelwalandGitHub ef108839c4 [RANTS-57] chore: replace target date with due date in work item filters dropdown (#6806) 2025-03-24 18:24:10 +05:30
Bavisetti NarayanandGitHub fe04e5a292 [WEB-3658] fix: remove cycles and modules when issues are bulk deleted (#6807) 2025-03-24 18:23:09 +05:30
Aaryan KhandelwalandGitHub 50e0cb7ffd [RANTS-75] chore: update profile sidebar icons and copy for consistency (#6808) 2025-03-24 18:21:12 +05:30
Akshita GoyalandGitHub d37d210921 [WEB-3677] fix: settings dynamic pages permissions (#6804)
* fix: settings dynamic pages permissions

* fix: refactor
2025-03-24 18:15:43 +05:30
Anmol Singh BhatiaandGitHub ab3eadf767 [WEB-3614] fix: cmd-k item focus state (#6800) 2025-03-24 18:13:49 +05:30
sriram veeraghanta dbdf2f001a fix: transpile packages for web application 2025-03-24 13:47:00 +05:30
Prateek ShouryaandGitHub 0d069bf46e [RANTS-65] fix: undefined work item sequence in bulk delete work item modal (#6797) 2025-03-24 13:41:02 +05:30
Prateek ShouryaandGitHub 962923ff4f fix: admin build (#6798) 2025-03-24 13:40:07 +05:30
Samuel TorresandGitHub f720a9afb2 feat: validate github organization during OAuth login (#6700)
* feat: add GITHUB_ORGANIZATION_ID support for GitHub OAuth integration

* fix: remove debug print statements from InstanceConfigurationEndpoint
2025-03-24 12:55:20 +05:30
4032aa62c5 [WEB-3551] fix: role improvements (#6763)
* Return Cycle start and end dates in project's timezone

* fix: role improvements

* chore: role updates

* chore: update role endpoint to update workspace admin permissions

* fix: conditions

* chore: update member role for workspace members

* chore: update workspace permission role

* fix: currentAdmin permissions

---------

Co-authored-by: Dheeraj Kumar Ketireddy <dheeru0198@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-03-24 12:52:57 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cbe248591e chore(deps): bump next in the npm_and_yarn group across 1 directory (#6796)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.24 to 14.2.25
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.24...v14.2.25)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  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-24 12:52:16 +05:30
Dheeraj Kumar KetireddyandGitHub 75a9b71edb [WEB-3513] fix: return cycle start and end dates in project's timezone 2025-03-24 12:51:44 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ef42ce04a4 chore(deps): bump gunicorn (#6793)
Bumps the pip group with 1 update in the /apiserver/requirements directory: [gunicorn](https://github.com/benoitc/gunicorn).


Updates `gunicorn` from 22.0.0 to 23.0.0
- [Release notes](https://github.com/benoitc/gunicorn/releases)
- [Commits](https://github.com/benoitc/gunicorn/compare/22.0.0...23.0.0)

---
updated-dependencies:
- dependency-name: gunicorn
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 12:48:05 +05:30
6bafdb6dd8 [PE-298] Fix: Copy markdown to clipboard (#6675)
* fix: markdown for mentions fixed

* fix: copying text in mentions

* fix: refactored the component to use the same function

* chore: renamed funcion name

* add the new copy extension

* init working fix

* remove useless code

* improve readibility

* update node import

* better smaller logic

* remove log

* add open close end handler

* update readabliity

* handle tables

* handle triple click in cell

* triple tap select current line

* handle block and list

* lists fixed

* handle all possible cases of copy in table

* update the min elements

* handle multi types in table

* handle table seletion cases

* handle whole table handler

* feat: all case converd

* update markdown handling code

* update return statement

* handle using group block

* handle param

* handle multple cell in table

* handle using recursion

* add types

* fix code rabbit  suggestions

* fix root node bug

* update recursion with loop

* update transform copied to false

* refactor clipboard extension: remove options and integrate MarkdownClipboard into core extensions

* fix: header and code handler

* fix: store hooks fixed

* fix: mention id

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
2025-03-24 12:32:11 +05:30
sriram veeraghanta 72307ec100 chore: update package version 2025-03-21 17:00:14 +05:30
sriram veeraghanta 2eb1d03c20 fix: transpile and optimize package imports 2025-03-21 01:51:50 +05:30
Prateek ShouryaandGitHub 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 BhatiaandGitHub 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
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
SangeethaandGitHub 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 KrishnaandGitHub 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
NikhilandGitHub 04fb13cbca chore: update celery cron configurations (#6776) 2025-03-20 14:02:55 +05:30
Vamsi KrishnaandGitHub 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 KrishnaandGitHub 433682e913 fix: space app icons display (#6784) 2025-03-20 13:58:48 +05:30
Aaryan KhandelwalandGitHub f181b671f3 chore: update year range in the calendar component (#6781) 2025-03-19 19:06:58 +05:30
Prateek ShouryaandGitHub f82d4a9ead [WEB-3589] improvement: reset language to default on sign out (#6775) 2025-03-19 14:42:19 +05:30
Anmol Singh BhatiaandGitHub 3f22642732 [WEB-3580] fix: automations settings translation (#6767) 2025-03-18 16:34:58 +05:30
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 KrishnaandGitHub 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 ShouryaandGitHub 1bf683e044 improvement: command palette search results (#6761) 2025-03-17 15:45:03 +05:30
Akshita GoyalandGitHub 807148671f fix: build (#6760) 2025-03-17 14:38:46 +05:30
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 KhandelwalandGitHub 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
191 changed files with 15632 additions and 2027 deletions
-50
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]
+4 -47
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
-3
View File
@@ -43,9 +43,6 @@ NGINX_PORT=80
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
+15
View File
@@ -43,6 +43,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
defaultValues: {
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
},
});
@@ -93,6 +94,19 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
error: Boolean(errors.GITHUB_CLIENT_SECRET),
required: true,
},
{
key: "GITHUB_ORGANIZATION_ID",
type: "text",
label: "Organization ID",
description: (
<>
The organization github ID.
</>
),
placeholder: "123456789",
error: Boolean(errors.GITHUB_ORGANIZATION_ID),
required: false,
},
];
const GITHUB_SERVICE_FIELD: TCopyField[] = [
@@ -150,6 +164,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
});
})
.catch((err) => console.error(err));
+13
View File
@@ -9,6 +9,19 @@ const nextConfig = {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
transpilePackages: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
};
module.exports = nextConfig;
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -21,7 +21,6 @@
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.54.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
@@ -30,7 +29,7 @@
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.20",
"next": "^14.2.25",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",
-3
View File
@@ -145,11 +145,8 @@ RUN chmod +x /app/pg-setup.sh
# APPLICATION ENVIRONMENT SETTINGS
# *****************************************************************************
ENV APP_DOMAIN=localhost
ENV WEB_URL=http://${APP_DOMAIN}
ENV DEBUG=0
ENV SENTRY_DSN=
ENV SENTRY_ENVIRONMENT=production
ENV CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN},https://${APP_DOMAIN}
# Secret Key
ENV SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5
-4
View File
@@ -3,10 +3,6 @@
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
# Error logs
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
# Database Settings
POSTGRES_USER="plane"
POSTGRES_PASSWORD="plane"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+9
View File
@@ -1,4 +1,5 @@
# Third party imports
import pytz
from rest_framework import serializers
# Module imports
@@ -18,6 +19,14 @@ class CycleSerializer(BaseSerializer):
completed_estimates = serializers.FloatField(read_only=True)
started_estimates = serializers.FloatField(read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
project = self.context.get("project")
if project and project.timezone:
project_timezone = pytz.timezone(project.timezone)
self.fields["start_date"].timezone = project_timezone
self.fields["end_date"].timezone = project_timezone
def validate(self, data):
if (
data.get("start_date", None) is not None
+15 -7
View File
@@ -137,10 +137,12 @@ class CycleAPIEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id, pk=None):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset, fields=self.fields,
expand=self.expand, context={"project": project}
).data
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
@@ -152,7 +154,8 @@ class CycleAPIEndpoint(BaseAPIView):
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
queryset, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data
return Response(data, status=status.HTTP_200_OK)
@@ -163,7 +166,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -174,7 +178,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -185,7 +190,8 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
@@ -198,14 +204,16 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
).data,
)
+14
View File
@@ -268,6 +268,20 @@ class IssueActivitySerializer(BaseSerializer):
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
source_data = serializers.SerializerMethodField()
def get_source_data(self, obj):
if (
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return {
"source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email,
"extra": obj.issue.source_data[0].extra,
}
return None
class Meta:
model = IssueActivity
+15 -6
View File
@@ -268,7 +268,7 @@ class CycleViewSet(BaseViewSet):
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
data, datetime_fields, project_timezone
)
return Response(data, status=status.HTTP_200_OK)
@@ -318,9 +318,13 @@ class CycleViewSet(BaseViewSet):
.first()
)
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
cycle, datetime_fields, project_timezone
)
# Send the model activity
@@ -407,9 +411,13 @@ class CycleViewSet(BaseViewSet):
"created_by",
).first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
cycle, datetime_fields, project_timezone
)
# Send the model activity
@@ -480,10 +488,11 @@ class CycleViewSet(BaseViewSet):
)
queryset = queryset.first()
# Fetch the project timezone
project = Project.objects.get(id=self.kwargs.get("project_id"))
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
data = user_timezone_converter(data, datetime_fields, project_timezone)
recent_visited_task.delay(
slug=slug,
+12 -3
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(
+8
View File
@@ -45,6 +45,7 @@ from plane.db.models import (
ProjectMember,
CycleIssue,
UserRecentVisit,
ModuleIssue,
)
from plane.utils.grouper import (
issue_group_values,
@@ -738,6 +739,13 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
total_issues = len(issues)
# First, delete all related cycle issues
CycleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Then, delete all related module issues
ModuleIssue.objects.filter(issue_id__in=issue_ids).delete()
# Finally, delete the issues themselves
issues.delete()
return Response(
@@ -177,7 +177,9 @@ class ProjectViewSet(BaseViewSet):
"module_view",
"page_view",
"inbox_view",
"guest_view_all_features",
"project_lead",
"network",
"created_at",
"updated_at",
"created_by",
+17 -7
View File
@@ -16,17 +16,17 @@ from rest_framework.permissions import AllowAny
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.app.serializers import ProjectMemberInviteSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
ProjectMember,
Workspace,
ProjectMemberInvite,
User,
WorkspaceMember,
Project,
IssueUserProperty,
)
from plane.db.models.project import ProjectNetwork
class ProjectInvitationsViewset(BaseViewSet):
@@ -128,6 +128,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
.select_related("workspace", "workspace__owner", "project")
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def create(self, request, slug):
project_ids = request.data.get("project_ids", [])
@@ -136,11 +137,20 @@ class UserProjectInvitationsViewset(BaseViewSet):
member=request.user, workspace__slug=slug, is_active=True
)
if workspace_member.role not in [ROLE.ADMIN.value, ROLE.MEMBER.value]:
return Response(
{"error": "You do not have permission to join the project"},
status=status.HTTP_403_FORBIDDEN,
)
# Get all the projects
projects = Project.objects.filter(
id__in=project_ids, workspace__slug=slug
).only("id", "network")
# Check if user has permission to join each project
for project in projects:
if (
project.network == ProjectNetwork.SECRET.value
and workspace_member.role != ROLE.ADMIN.value
):
return Response(
{"error": "Only workspace admins can join private project"},
status=status.HTTP_403_FORBIDDEN,
)
workspace_role = workspace_member.role
workspace = workspace_member.workspace
+12 -18
View File
@@ -10,11 +10,7 @@ from plane.app.serializers import (
ProjectMemberRoleSerializer,
)
from plane.app.permissions import (
ProjectMemberPermission,
ProjectLitePermission,
WorkspaceUserPermission,
)
from plane.app.permissions import WorkspaceUserPermission
from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
@@ -26,14 +22,6 @@ class ProjectMemberViewSet(BaseViewSet):
serializer_class = ProjectMemberAdminSerializer
model = ProjectMember
def get_permissions(self):
if self.action == "leave":
self.permission_classes = [ProjectLitePermission]
else:
self.permission_classes = [ProjectMemberPermission]
return super(ProjectMemberViewSet, self).get_permissions()
search_fields = ["member__display_name", "member__first_name"]
def get_queryset(self):
@@ -187,12 +175,20 @@ class ProjectMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN])
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, is_active=True
)
if request.user.id == project_member.member_id:
# Fetch the workspace role of the project member
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
is_workspace_admin = workspace_role == ROLE.ADMIN.value
# Check if the user is not editing their own role if they are not an admin
if request.user.id == project_member.member_id and not is_workspace_admin:
return Response(
{"error": "You cannot update your own role"},
status=status.HTTP_400_BAD_REQUEST,
@@ -205,9 +201,6 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
)
workspace_role = WorkspaceMember.objects.get(
workspace__slug=slug, member=project_member.member, is_active=True
).role
if workspace_role in [5] and int(
request.data.get("role", project_member.role)
) in [15, 20]:
@@ -222,6 +215,7 @@ class ProjectMemberViewSet(BaseViewSet):
"role" in request.data
and int(request.data.get("role", project_member.role))
> requested_project_member.role
and not is_workspace_admin
):
return Response(
{"error": "You cannot update a role that is higher than your own role"},
+1 -1
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)
@@ -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(
@@ -68,10 +68,11 @@ class WorkSpaceMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
if workspace_member.role > int(request.data.get("role")):
_ = ProjectMember.objects.filter(
# If a user is moved to a guest role he can't have any other role in projects
if "role" in request.data and int(request.data.get("role")) == 5:
ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id
).update(role=int(request.data.get("role")))
).update(role=5)
serializer = WorkSpaceMemberSerializer(
workspace_member, data=request.data, partial=True
@@ -36,10 +36,12 @@ AUTHENTICATION_ERROR_CODES = {
"OAUTH_NOT_CONFIGURED": 5104,
"GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5110,
"GITHUB_USER_NOT_IN_ORG": 5122,
"GITLAB_NOT_CONFIGURED": 5111,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
"GITLAB_OAUTH_PROVIDER_ERROR": 5121,
# Reset Password
"INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5130,
@@ -18,11 +18,16 @@ from plane.authentication.adapter.error import (
class GitHubOAuthProvider(OauthAdapter):
token_url = "https://github.com/login/oauth/access_token"
userinfo_url = "https://api.github.com/user"
org_membership_url = f"https://api.github.com/orgs"
provider = "github"
scope = "read:user user:email"
organization_scope = "read:org"
def __init__(self, request, code=None, state=None, callback=None):
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET = get_configuration_value(
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_ORGANIZATION_ID = get_configuration_value(
[
{
"key": "GITHUB_CLIENT_ID",
@@ -32,6 +37,10 @@ class GitHubOAuthProvider(OauthAdapter):
"key": "GITHUB_CLIENT_SECRET",
"default": os.environ.get("GITHUB_CLIENT_SECRET"),
},
{
"key": "GITHUB_ORGANIZATION_ID",
"default": os.environ.get("GITHUB_ORGANIZATION_ID"),
},
]
)
@@ -43,6 +52,10 @@ class GitHubOAuthProvider(OauthAdapter):
client_id = GITHUB_CLIENT_ID
client_secret = GITHUB_CLIENT_SECRET
self.organization_id = GITHUB_ORGANIZATION_ID
if self.organization_id:
self.scope += f" {self.organization_scope}"
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
url_params = {
@@ -113,12 +126,26 @@ class GitHubOAuthProvider(OauthAdapter):
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
)
def is_user_in_organization(self, github_username):
headers = {"Authorization": f"Bearer {self.token_data.get('access_token')}"}
response = requests.get(f"{self.org_membership_url}/{self.organization_id}/memberships/{github_username}", headers=headers)
return response.status_code == 200 # 200 means the user is a member
def set_user_data(self):
user_info_response = self.get_user_response()
headers = {
"Authorization": f"Bearer {self.token_data.get('access_token')}",
"Accept": "application/json",
}
if self.organization_id:
if not self.is_user_in_organization(user_info_response.get("login")):
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["GITHUB_USER_NOT_IN_ORG"],
error_message="GITHUB_USER_NOT_IN_ORG",
)
email = self.__get_email(headers=headers)
super().set_user_data(
{
+24 -23
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
},
}
@@ -3,7 +3,6 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from sentry_sdk import capture_exception
import uuid
@@ -29,7 +28,6 @@ def create_issue_relation(apps, schema_editor):
)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):
+10
View File
@@ -1,6 +1,7 @@
# Python imports
import pytz
from uuid import uuid4
from enum import Enum
# Django imports
from django.conf import settings
@@ -17,6 +18,15 @@ from .base import BaseModel
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
class ProjectNetwork(Enum):
SECRET = 0
PUBLIC = 2
@classmethod
def choices(cls):
return [(0, "Secret"), (2, "Public")]
def get_default_props():
return {
"filters": {
@@ -71,6 +71,12 @@ class Command(BaseCommand):
"category": "GITHUB",
"is_encrypted": True,
},
{
"key": "GITHUB_ORGANIZATION_ID",
"value": os.environ.get("GITHUB_ORGANIZATION_ID"),
"category": "GITHUB",
"is_encrypted": False,
},
{
"key": "GITLAB_HOST",
"value": os.environ.get("GITLAB_HOST"),
-23
View File
@@ -7,13 +7,9 @@ from urllib.parse import urlparse
# Third party imports
import dj_database_url
import sentry_sdk
# Django imports
from django.core.management.utils import get_random_secret_key
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.redis import RedisIntegration
from corsheaders.defaults import default_headers
@@ -267,25 +263,6 @@ CELERY_IMPORTS = (
"plane.bgtasks.issue_description_version_sync",
)
# Sentry Settings
# Enable Sentry Settings
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get(
"SENTRY_DSN"
).startswith("https://"):
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN", ""),
integrations=[
DjangoIntegration(),
RedisIntegration(),
CeleryIntegration(monitor_beat_tasks=True),
],
traces_sample_rate=1,
send_default_pii=True,
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
profiles_sample_rate=float(os.environ.get("SENTRY_PROFILE_SAMPLE_RATE", 0)),
)
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
# Unsplash Access key
@@ -5,9 +5,6 @@ import traceback
# Django imports
from django.conf import settings
# Third party imports
from sentry_sdk import capture_exception
def log_exception(e):
# Log the error
@@ -18,6 +15,4 @@ def log_exception(e):
# Print the traceback if in debug mode
print(traceback.format_exc())
# Capture in sentry if configured
capture_exception(e)
return
-2
View File
@@ -26,8 +26,6 @@ faker==25.0.0
django-filter==24.2
# json model
jsonmodels==2.7.0
# sentry
sentry-sdk==2.8.0
# storage
django-storages==1.14.2
# user management
+1 -1
View File
@@ -1,3 +1,3 @@
-r base.txt
# server
gunicorn==22.0.0
gunicorn==23.0.0
+3 -15
View File
@@ -6,16 +6,8 @@
"website": "https://plane.so/",
"success_url": "/",
"stack": "heroku-22",
"keywords": [
"plane",
"project management",
"django",
"next"
],
"addons": [
"heroku-postgresql:mini",
"heroku-redis:mini"
],
"keywords": ["plane", "project management", "django", "next"],
"addons": ["heroku-postgresql:mini", "heroku-redis:mini"],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-python.git"
@@ -61,10 +53,6 @@
"description": "AWS Bucket Name to use for S3",
"value": ""
},
"SENTRY_DSN": {
"description": "",
"value": ""
},
"WEB_URL": {
"description": "Web URL for Plane this will be used for redirections in the emails",
"value": ""
@@ -82,4 +70,4 @@
"value": ""
}
}
}
}
-2
View File
@@ -42,8 +42,6 @@ x-live-env: &live-env
x-app-env: &app-env
WEB_URL: ${WEB_URL:-http://localhost}
DEBUG: ${DEBUG:-0}
SENTRY_DSN: ${SENTRY_DSN}
SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
GUNICORN_WORKERS: 1
USE_MINIO: ${USE_MINIO:-1}
-2
View File
@@ -12,8 +12,6 @@ LIVE_REPLICAS=1
NGINX_PORT=80
WEB_URL=http://${APP_DOMAIN}
DEBUG=0
SENTRY_DSN=
SENTRY_ENVIRONMENT=production
CORS_ALLOWED_ORIGINS=http://${APP_DOMAIN}
API_BASE_URL=http://api:8000
+2 -4
View File
@@ -1,13 +1,13 @@
{
"name": "live",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
"private": true,
"type": "module",
"scripts": {
"dev": "concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"dev": "PORT=3100 concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
"start": "node dist/server.js",
"lint": "eslint src --ext .ts,.tsx",
@@ -23,8 +23,6 @@
"@plane/constants": "*",
"@plane/editor": "*",
"@plane/types": "*",
"@sentry/node": "^9.0.1",
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
"axios": "^1.8.3",
-19
View File
@@ -1,19 +0,0 @@
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
// Ensure to call this before importing any other modules!
Sentry.init({
dsn: process.env.LIVE_SENTRY_DSN,
environment: process.env.LIVE_SENTRY_ENVIRONMENT || "development",
integrations: [
// Add our Profiling integration
nodeProfilingIntegration(),
],
// Add Tracing by setting tracesSampleRate
// We recommend adjusting this value in production
tracesSampleRate: Number(process.env.LIVE_SENTRY_TRACES_SAMPLE_RATE) || 0.5,
// Set sampling rate for profiling
// This is relative to tracesSampleRate
profilesSampleRate: 1.0,
});
+1 -6
View File
@@ -1,11 +1,8 @@
import * as Sentry from "@sentry/node";
import compression from "compression";
import cors from "cors";
import expressWs from "express-ws";
import express from "express";
import helmet from "helmet";
// config
import "@/core/config/sentry-config.js";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
@@ -15,7 +12,7 @@ import { errorHandler } from "@/core/helpers/error-handler.js";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
const app = express();
const app: any = express();
expressWs(app);
app.set("port", process.env.PORT || 3000);
@@ -92,8 +89,6 @@ app.use((_req, res) => {
res.status(404).send("Not Found");
});
Sentry.setupExpressErrorHandler(app);
app.use(errorHandler);
const liveServer = app.listen(app.get("port"), () => {
-3
View File
@@ -16,9 +16,6 @@
"skipLibCheck": true,
"sourceMap": true,
"inlineSources": true,
// Set `sourceRoot` to "/" to strip the build path prefix
// from generated source code references.
// This improves issue grouping in Sentry.
"sourceRoot": "/"
},
"include": ["src/**/*.ts", "tsup.config.ts"],
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"main": "./src/index.ts",
"license": "AGPL-3.0"
+1
View File
@@ -339,6 +339,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
"-updated_at",
"start_date",
"-priority",
"target_date",
],
type: [null, "active", "backlog"],
},
+6 -9
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 = [
+20 -3
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 = [
@@ -83,14 +83,14 @@ export const WORKSPACE_SETTINGS = {
key: "general",
i18n_label: "workspace_settings.settings.general.title",
href: `/settings`,
access: [EUserWorkspaceRoles.ADMIN],
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/`,
},
members: {
key: "members",
i18n_label: "workspace_settings.settings.members.title",
href: `/settings/members`,
access: [EUserWorkspaceRoles.ADMIN],
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/members/`,
},
"billing-and-plans": {
@@ -123,6 +123,10 @@ export const WORKSPACE_SETTINGS = {
},
};
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
Object.entries(WORKSPACE_SETTINGS).map(([_, { href, access }]) => [href, access])
);
export const WORKSPACE_SETTINGS_LINKS: {
key: string;
i18n_label: string;
@@ -324,3 +328,16 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS_LINKS: IWorkspaceSidebarN
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["inbox"],
WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS["projects"],
];
export const IS_FAVORITE_MENU_OPEN = "is_favorite_menu_open";
export const WORKSPACE_DEFAULT_SEARCH_RESULT: IWorkspaceSearchResults = {
results: {
workspace: [],
project: [],
issue: [],
cycle: [],
module: [],
issue_view: [],
page: [],
},
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.25.2",
"version": "0.25.3",
"description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true,
@@ -0,0 +1,89 @@
import { Extension } from "@tiptap/core";
import { Fragment, Node } from "@tiptap/pm/model";
import { Plugin, PluginKey } from "@tiptap/pm/state";
export const MarkdownClipboard = Extension.create({
name: "markdownClipboard",
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("markdownClipboard"),
props: {
clipboardTextSerializer: (slice) => {
const markdownSerializer = this.editor.storage.markdown.serializer;
const isTableRow = slice.content.firstChild?.type?.name === "tableRow";
const nodeSelect = slice.openStart === 0 && slice.openEnd === 0;
if (nodeSelect) {
return markdownSerializer.serialize(slice.content);
}
const processTableContent = (tableNode: Node | Fragment) => {
let result = "";
tableNode.content?.forEach?.((tableRowNode: Node | Fragment) => {
tableRowNode.content?.forEach?.((cell: Node) => {
const cellContent = cell.content ? markdownSerializer.serialize(cell.content) : "";
result += cellContent + "\n";
});
});
return result;
};
if (isTableRow) {
const rowsCount = slice.content?.childCount || 0;
const cellsCount = slice.content?.firstChild?.content?.childCount || 0;
if (rowsCount === 1 || cellsCount === 1) {
return processTableContent(slice.content);
} else {
return markdownSerializer.serialize(slice.content);
}
}
const traverseToParentOfLeaf = (
node: Node | null,
parent: Fragment | Node,
depth: number
): Node | Fragment => {
let currentNode = node;
let currentParent = parent;
let currentDepth = depth;
while (currentNode && currentDepth > 1 && currentNode.content?.firstChild) {
if (currentNode.content?.childCount > 1) {
if (currentNode.content.firstChild?.type?.name === "listItem") {
return currentParent;
} else {
return currentNode.content;
}
}
currentParent = currentNode;
currentNode = currentNode.content?.firstChild || null;
currentDepth--;
}
return currentParent;
};
if (slice.content.childCount > 1) {
return markdownSerializer.serialize(slice.content);
} else {
const targetNode = traverseToParentOfLeaf(slice.content.firstChild, slice.content, slice.openStart);
let currentNode = targetNode;
while (currentNode && currentNode.content && currentNode.childCount === 1 && currentNode.firstChild) {
currentNode = currentNode.firstChild;
}
if (currentNode instanceof Node && currentNode.isText) {
return currentNode.text;
}
return markdownSerializer.serialize(targetNode);
}
},
},
}),
];
},
});
@@ -29,6 +29,7 @@ import {
TableCell,
TableHeader,
TableRow,
MarkdownClipboard,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -130,10 +131,11 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: true,
transformCopiedText: false,
transformPastedText: true,
breaks: true,
}),
MarkdownClipboard,
Table,
TableHeader,
TableCell,
@@ -23,3 +23,4 @@ export * from "./quote";
export * from "./read-only-extensions";
export * from "./side-menu";
export * from "./text-align";
export * from "./clipboard";
@@ -1,12 +1,15 @@
import { mergeAttributes } from "@tiptap/core";
import Mention, { MentionOptions } from "@tiptap/extension-mention";
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
import { Node as NodeType } from "@tiptap/pm/model";
// types
import { TMentionHandler } from "@/types";
// local types
import { EMentionComponentAttributeNames } from "./types";
import { EMentionComponentAttributeNames, TMentionComponentAttributes } from "./types";
export type TMentionExtensionOptions = MentionOptions & {
renderComponent: TMentionHandler["renderComponent"];
getMentionedEntityDetails: TMentionHandler["getMentionedEntityDetails"];
};
export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOptions>({
@@ -40,9 +43,26 @@ export const CustomMentionExtensionConfig = Mention.extend<TMentionExtensionOpti
class: "mention",
},
addStorage(this) {
renderText({ node }) {
return getMentionDisplayText(this.options, node);
},
addStorage() {
const options = this.options;
return {
mentionsOpen: false,
markdown: {
serialize(state: MarkdownSerializerState, node: NodeType) {
state.write(getMentionDisplayText(options, node));
},
},
};
},
});
function getMentionDisplayText(options: TMentionExtensionOptions, node: NodeType): string {
const attrs = node.attrs as TMentionComponentAttributes;
const mentionEntityId = attrs[EMentionComponentAttributeNames.ENTITY_IDENTIFIER];
const mentionEntityDetails = options.getMentionedEntityDetails?.(mentionEntityId ?? "");
return `@${mentionEntityDetails?.display_name ?? attrs[EMentionComponentAttributeNames.ID] ?? mentionEntityId}`;
}
@@ -9,12 +9,13 @@ import { MentionNodeView } from "./mention-node-view";
import { renderMentionsDropdown } from "./utils";
export const CustomMentionExtension = (props: TMentionHandler) => {
const { searchCallback, renderComponent } = props;
const { searchCallback, renderComponent, getMentionedEntityDetails } = props;
return CustomMentionExtensionConfig.extend({
addOptions(this) {
return {
...this.parent?.(),
renderComponent,
getMentionedEntityDetails,
};
},
@@ -24,6 +24,7 @@ import {
CustomTextAlignExtension,
CustomCalloutReadOnlyExtension,
CustomColorExtension,
MarkdownClipboard,
} from "@/extensions";
// helpers
import { isValidHttpUrl } from "@/helpers/common";
@@ -114,8 +115,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
CustomCodeInlineExtension,
Markdown.configure({
html: true,
transformCopiedText: true,
transformCopiedText: false,
}),
MarkdownClipboard,
Table,
TableHeader,
TableCell,
@@ -16,6 +16,22 @@ export function tableControls() {
},
},
props: {
handleTripleClickOn(view, pos, node, nodePos, event, direct) {
if (node.type.name === 'tableCell') {
event.preventDefault();
const $pos = view.state.doc.resolve(pos);
const line = $pos.parent;
const linePos = $pos.start();
const start = linePos;
const end = linePos + line.nodeSize - 1;
const tr = view.state.tr.setSelection(
TextSelection.create(view.state.doc, start, end)
);
view.dispatch(tr);
return true;
}
return false;
},
handleDOMEvents: {
mousemove: (view, event) => {
const pluginState = key.getState(view.state);
+2 -1
View File
@@ -1,5 +1,5 @@
// plane types
import { TSearchEntities } from "@plane/types";
import { IUserLite, TSearchEntities } from "@plane/types";
export type TMentionSuggestion = {
entity_identifier: string;
@@ -20,6 +20,7 @@ export type TMentionComponentProps = Pick<TMentionSuggestion, "entity_identifier
export type TReadOnlyMentionHandler = {
renderComponent: (props: TMentionComponentProps) => React.ReactNode;
getMentionedEntityDetails?: (entity_identifier: string) => { display_name: string } | undefined;
};
export type TMentionHandler = TReadOnlyMentionHandler & {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"files": [
"library.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally",
"private": true,
+8 -2
View File
@@ -7,10 +7,16 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Français", value: "fr" },
{ label: "Español", value: "es" },
{ label: "日本語", value: "ja" },
{ label: "中文", value: "zh-CN" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
{ label: "Русский", value: "ru" },
{ label: "Italian", value: "it" },
{ label: "Čeština", value: "cs" },
{ label: "Slovenčina", value: "sk" },
{ label: "Deutsch", value: "de" },
{ label: "Українська", value: "ua" },
{ label: "Polski", value: "pl" },
{ label: "한국어", value: "ko" },
];
export const STORAGE_KEY = "userLanguage";
export const LANGUAGE_STORAGE_KEY = "userLanguage";
+4 -4
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 {
@@ -6,7 +6,7 @@
"home": "Domov",
"your_work": "Vaše práce",
"inbox": "Doručená pošta",
"workspace": "workspace",
"workspace": "Pracovní prostor",
"views": "Pohledy",
"analytics": "Analytika",
"work_items": "Pracovní položky",
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@
"workspace": "Workspace",
"views": "Views",
"analytics": "Analytics",
"work_items": "Work Items",
"work_items": "Work items",
"cycles": "Cycles",
"modules": "Modules",
"intake": "Intake",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -39
View File
@@ -3,7 +3,7 @@ import get from "lodash/get";
import merge from "lodash/merge";
import { makeAutoObservable, runInAction } from "mobx";
// constants
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, STORAGE_KEY } from "../constants";
import { FALLBACK_LANGUAGE, SUPPORTED_LANGUAGES, LANGUAGE_STORAGE_KEY } from "../constants";
// core translations imports
import coreEn from "../locales/en/core.json";
// types
@@ -48,14 +48,14 @@ export class TranslationStore {
private initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem(STORAGE_KEY) as TLanguage;
const savedLocale = localStorage.getItem(LANGUAGE_STORAGE_KEY) as TLanguage;
if (this.isValidLanguage(savedLocale)) {
this.setLanguage(savedLocale);
return;
}
const browserLang = this.getBrowserLanguage();
this.setLanguage(browserLang);
// Fallback to default language
this.setLanguage(FALLBACK_LANGUAGE);
}
/** Loads the translations for the current language */
@@ -147,12 +147,24 @@ export class TranslationStore {
return import("../locales/ja/translations.json");
case "zh-CN":
return import("../locales/zh-CN/translations.json");
case "zh-TW":
return import("../locales/zh-TW/translations.json");
case "ru":
return import("../locales/ru/translations.json");
case "it":
return import("../locales/it/translations.json");
case "cs":
return import("../locales/cs/translations.json");
case "sk":
return import("../locales/sk/translations.json");
case "de":
return import("../locales/de/translations.json");
case "ua":
return import("../locales/ua/translations.json");
case "pl":
return import("../locales/pl/translations.json");
case "ko":
return import("../locales/ko/translations.json");
default:
throw new Error(`Unsupported language: ${language}`);
}
@@ -163,40 +175,6 @@ export class TranslationStore {
return lang !== null && this.availableLanguages.some((l) => l.value === lang);
}
/** Checks if a language code is similar to any supported language */
private findSimilarLanguage(lang: string): TLanguage | null {
// Convert to lowercase for case-insensitive comparison
const normalizedLang = lang.toLowerCase();
// Find a supported language that includes or is included in the browser language
const similarLang = this.availableLanguages.find(
(l) => normalizedLang.includes(l.value.toLowerCase()) || l.value.toLowerCase().includes(normalizedLang)
);
return similarLang ? similarLang.value : null;
}
/** Gets the browser language based on the navigator.language */
private getBrowserLanguage(): TLanguage {
const browserLang = navigator.language;
// Check exact match first
if (this.isValidLanguage(browserLang)) {
return browserLang;
}
// Check base language without region code
const baseLang = browserLang.split("-")[0];
if (this.isValidLanguage(baseLang)) {
return baseLang as TLanguage;
}
// Try to find a similar language
const similarLang = this.findSimilarLanguage(browserLang) || this.findSimilarLanguage(baseLang);
return similarLang || FALLBACK_LANGUAGE;
}
/**
* Gets the cache key for the given key and locale
* @param key - the key to get the cache key for
@@ -281,7 +259,7 @@ export class TranslationStore {
}
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, lng);
localStorage.setItem(LANGUAGE_STORAGE_KEY, lng);
document.documentElement.lang = lng;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "ru" | "it" | "cs";
export type TLanguage = "en" | "fr" | "es" | "ja" | "zh-CN" | "zh-TW" | "ru" | "it" | "cs" | "sk" | "de" | "ua" | "pl" | "ko";
export interface ILanguageOption {
label: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/logger",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "Logger shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/propel",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"license": "AGPL-3.0",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/services",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"main": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/shared-state",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "Shared state shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/tailwind-config",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"description": "common tailwind configuration across monorepo",
"main": "tailwind.config.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/types",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"types": "./src/index.d.ts",
+6
View File
@@ -6,6 +6,12 @@ export enum EUserPermissions {
export type TUserPermissions = EUserPermissions.ADMIN | EUserPermissions.MEMBER | EUserPermissions.GUEST;
// project network
export enum EProjectNetwork {
PRIVATE = 0,
PUBLIC = 2,
}
// project pages
export enum EPageAccess {
PUBLIC = 0,
+3 -1
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;
+2 -1
View File
@@ -21,7 +21,8 @@ export type TInstanceGoogleAuthenticationConfigurationKeys =
export type TInstanceGithubAuthenticationConfigurationKeys =
| "GITHUB_CLIENT_ID"
| "GITHUB_CLIENT_SECRET";
| "GITHUB_CLIENT_SECRET"
| "GITHUB_ORGANIZATION_ID";
export type TInstanceGitlabAuthenticationConfigurationKeys =
| "GITLAB_HOST"
@@ -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 = {
+2 -4
View File
@@ -25,7 +25,9 @@ export interface IPartialProject {
module_view: boolean;
page_view: boolean;
inbox_view: boolean;
guest_view_all_features?: boolean;
project_lead?: IUserLite | string | null;
network?: number;
// Timestamps
created_at?: Date;
updated_at?: Date;
@@ -46,13 +48,9 @@ export interface IProject extends IPartialProject {
default_state?: string | null;
description?: string;
estimate?: string | null;
guest_view_all_features?: boolean;
anchor?: string | null;
is_favorite?: boolean;
is_issue_type_enabled?: boolean;
is_time_tracking_enabled?: boolean;
members?: string[];
network?: number;
timezone?: string;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/typescript-config",
"version": "0.25.2",
"version": "0.25.3",
"license": "AGPL-3.0",
"private": true,
"files": [
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.25.2",
"version": "0.25.3",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+73 -68
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}
/>
);
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/utils",
"version": "0.25.2",
"version": "0.25.3",
"description": "Helper functions shared across multiple apps internally",
"license": "AGPL-3.0",
"private": true,
@@ -21,7 +21,7 @@ export const OAuthOptions: React.FC = observer(() => {
<GoogleOAuthButton text="Sign in with Google" />
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with Github" />}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with GitHub" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
</div>
</>
@@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
@@ -17,22 +19,29 @@ type LiteTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn(props.containerClassName, "relative p-2")}
/>
)
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<LiteTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn(props.containerClassName, "relative p-2")}
/>
);
}
);
LiteTextReadOnlyEditor.displayName = "LiteTextReadOnlyEditor";
@@ -6,6 +6,8 @@ import { MakeOptional } from "@plane/types";
import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
interface RichTextEditorWrapperProps
extends MakeOptional<Omit<IRichTextEditor, "fileHandler" | "mentionHandler">, "disabledExtensions"> {
@@ -16,11 +18,14 @@ interface RichTextEditorWrapperProps
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
const { anchor, containerClassName, uploadFile, workspaceId, disabledExtensions, ...rest } = props;
const { getMemberById } = useMember();
return (
<RichTextEditorWithRef
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
ref={ref}
disabledExtensions={disabledExtensions ?? []}
@@ -31,7 +36,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"
/>
);
});
@@ -7,6 +7,8 @@ import { EditorMentionsRoot } from "@/components/editor";
// helpers
import { cn } from "@/helpers/common.helper";
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
// store hooks
import { useMember } from "@/hooks/store";
type RichTextReadOnlyEditorWrapperProps = MakeOptional<
Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler">,
@@ -17,22 +19,29 @@ type RichTextReadOnlyEditorWrapperProps = MakeOptional<
};
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
/>
)
({ anchor, workspaceId, disabledExtensions, ...props }, ref) => {
const { getMemberById } = useMember();
return (
<RichTextReadOnlyEditorWithRef
ref={ref}
disabledExtensions={disabledExtensions ?? []}
fileHandler={getReadOnlyEditorFileHandlers({
anchor,
workspaceId,
})}
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
getMentionedEntityDetails: (id: string) => ({
display_name: getMemberById(id)?.member__display_name ?? "",
}),
}}
{...props}
// overriding the customClassName to add relative class passed
containerClassName={cn("relative p-0 border-none", props.containerClassName)}
/>
);
}
);
RichTextReadOnlyEditor.displayName = "RichTextReadOnlyEditor";
@@ -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>
-11
View File
@@ -1,11 +0,0 @@
// google.d.ts
interface GsiButtonConfiguration {
type: "standard" | "icon";
theme?: "outline" | "filled_blue" | "filled_black";
size?: "large" | "medium" | "small";
text?: "signin_with" | "signup_with" | "continue_with" | "signup_with";
shape?: "rectangular" | "pill" | "circle" | "square";
logo_alignment?: "left" | "center";
width?: number;
local?: string;
}
-9
View File
@@ -1,9 +0,0 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}
+14 -44
View File
@@ -1,8 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-disable @typescript-eslint/no-var-requires */
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: ".env" });
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
trailingSlash: true,
@@ -27,45 +23,19 @@ const nextConfig = {
],
unoptimized: true,
},
transpilePackages: [
"@plane/constants",
"@plane/editor",
"@plane/hooks",
"@plane/i18n",
"@plane/logger",
"@plane/propel",
"@plane/services",
"@plane/shared-state",
"@plane/types",
"@plane/ui",
"@plane/utils",
],
};
const sentryConfig = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: process.env.SENTRY_ORG_ID || "plane-hq",
project: process.env.SENTRY_PROJECT_ID || "plane-space",
authToken: process.env.SENTRY_AUTH_TOKEN,
// Only print logs for uploading source maps in CI
silent: true,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
};
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
module.exports = withSentryConfig(nextConfig, sentryConfig);
} else {
module.exports = nextConfig;
}
module.exports = nextConfig;
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.25.2",
"version": "0.25.3",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@@ -25,7 +25,6 @@
"@plane/types": "*",
"@plane/ui": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.54.0",
"axios": "^1.8.3",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
@@ -37,7 +36,7 @@
"mobx": "^6.10.0",
"mobx-react": "^9.1.1",
"mobx-utils": "^6.0.8",
"next": "^14.2.20",
"next": "^14.2.25",
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "^18.3.1",
-31
View File
@@ -1,31 +0,0 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});
-17
View File
@@ -1,17 +0,0 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
-3
View File
@@ -1,3 +0,0 @@
defaults.url=https://sentry.io/
defaults.org=plane
defaults.project=plane-space
-19
View File
@@ -1,19 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
});
+1 -7
View File
@@ -18,13 +18,7 @@
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
"NEXT_PUBLIC_SUPPORT_EMAIL",
"SENTRY_AUTH_TOKEN",
"SENTRY_ORG_ID",
"SENTRY_PROJECT_ID",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_MONITORING_ENABLED"
"NEXT_PUBLIC_SUPPORT_EMAIL"
],
"tasks": {
"build": {
@@ -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],
@@ -15,10 +15,12 @@ const MembersSettingsPage = observer(() => {
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
// derived values
const pageTitle = currentProjectDetails?.name ? `${currentProjectDetails?.name} - Members` : undefined;
const canPerformProjectMemberActions = allowPermissions(
const isProjectMemberOrAdmin = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT
);
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const canPerformProjectMemberActions = isProjectMemberOrAdmin || isWorkspaceAdmin;
if (workspaceUserInfo && !canPerformProjectMemberActions) {
return <NotAuthorizedView section="settings" isProjectView />;
@@ -3,7 +3,8 @@
import { FC, ReactNode } from "react";
import { observer } from "mobx-react";
// components
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useParams, usePathname } from "next/navigation";
import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants";
import { NotAuthorizedView } from "@/components/auth-screens";
import { AppHeader } from "@/components/core";
// hooks
@@ -21,17 +22,26 @@ export interface IWorkspaceSettingLayout {
const WorkspaceSettingLayout: FC<IWorkspaceSettingLayout> = observer((props) => {
const { children } = props;
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { workspaceUserInfo } = useUserPermissions();
const pathname = usePathname();
const [workspaceSlug, suffix, route] = pathname.replace(/^\/|\/$/g, "").split("/"); // Regex removes leading and trailing slashes
// derived values
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
const userWorkspaceRole = workspaceUserInfo?.[workspaceSlug.toString()]?.role;
const isAuthorized =
pathname &&
workspaceSlug &&
userWorkspaceRole &&
WORKSPACE_SETTINGS_ACCESS[route ? `/${suffix}/${route}` : `/${suffix}`]?.includes(
userWorkspaceRole as EUserWorkspaceRoles
);
return (
<>
<AppHeader header={<WorkspaceSettingHeader />} />
<MobileWorkspaceSettingsTabs />
<div className="inset-y-0 flex flex-row vertical-scrollbar scrollbar-lg h-full w-full overflow-y-auto">
{workspaceUserInfo && !isWorkspaceAdmin ? (
{workspaceUserInfo && !isAuthorized ? (
<NotAuthorizedView section="settings" />
) : (
<>
+8 -8
View File
@@ -9,13 +9,13 @@ import {
ChevronLeft,
LogOut,
MoveLeft,
Plus,
UserPlus,
Activity,
Bell,
CircleUser,
KeyRound,
Settings2,
CirclePlus,
Mails,
} from "lucide-react";
// plane imports
import { PROFILE_ACTION_LINKS } from "@plane/constants";
@@ -35,14 +35,14 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
const WORKSPACE_ACTION_LINKS = [
{
key: "create_workspace",
Icon: Plus,
label: "Create workspace",
Icon: CirclePlus,
i18n_label: "create_workspace",
href: "/create-workspace",
},
{
key: "invitations",
Icon: UserPlus,
label: "Invitations",
Icon: Mails,
i18n_label: "workspace_invites",
href: "/invitations",
},
];
@@ -243,8 +243,8 @@ export const ProfileLayoutSidebar = observer(() => {
sidebarCollapsed ? "justify-center" : ""
}`}
>
{<link.Icon className="h-4 w-4" />}
{!sidebarCollapsed && t(link.key)}
{<link.Icon className="flex-shrink-0 size-4" />}
{!sidebarCollapsed && t(link.i18n_label)}
</div>
</Tooltip>
</Link>

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