Compare commits

...

40 Commits

Author SHA1 Message Date
Aaryan Khandelwal 2e92f4d58b chore: update loader init logic 2024-11-22 14:20:50 +05:30
Aaryan Khandelwal e37db8d8f4 dev: pages trash 2024-11-22 13:31:34 +05:30
pablohashescobar ca91d5909b chore: formatting errors 2024-11-20 13:00:13 +05:30
pablohashescobar 3bea2e8d1b chore: fix instance apis 2024-11-20 12:35:13 +05:30
sriram veeraghanta 1325064676 fix: typo and naming conventions 2024-11-20 00:32:30 +05:30
sriram veeraghanta a01a371767 fix: typo fixes 2024-11-20 00:00:04 +05:30
sriram veeraghanta 2d60337eac fix: celery timestamp changes 2024-11-19 20:17:53 +05:30
pablohashescobar f3ac26e5c9 chore: instances 2024-11-19 19:47:13 +05:30
Aaryan Khandelwal d5a55de17a fix: cover image update fix for project and user profile (#6075)
* fix: cover image update payload

* fix: cover image assets

* chore: add gif support

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-11-19 18:28:53 +05:30
Prateek Shourya 6f497b024b [WEB-2770] fix: inbox issue detail loader on focus change (#6074) 2024-11-19 17:07:32 +05:30
Nikhil a3e8ee6045 fix: remove caching for user based apis to handle avatar uploads (#6072) 2024-11-19 15:42:10 +05:30
sriram veeraghanta c1ac6e4244 chore: removing dependabot updates alerts 2024-11-18 12:06:13 +05:30
dependabot[bot] 6d98619082 chore(deps): bump actions/checkout from 3 to 4 (#6005)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:42:08 +05:30
dependabot[bot] 52d3169542 chore(deps): bump softprops/action-gh-release from 2.0.8 to 2.1.0 (#6010)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.8 to 2.1.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.8...v2.1.0)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:40:41 +05:30
dependabot[bot] 5989b1a134 chore(deps): bump github/codeql-action from 2 to 3 (#6011)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 19:39:31 +05:30
sriram veeraghanta 291bb5c899 Merge branch 'preview' of github.com:makeplane/plane into preview 2024-11-16 19:37:22 +05:30
sriram veeraghanta 2ef00efaab fix: tubro repo upgrade 2024-11-16 19:37:06 +05:30
dependabot[bot] c5f96466e9 chore(deps): bump cross-spawn in the npm_and_yarn group (#6038)
Bumps the npm_and_yarn group with 1 update: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  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>
2024-11-16 18:40:01 +05:30
sriram veeraghanta 35938b57af fix: dependabot security patch only 2024-11-16 18:36:47 +05:30
dependabot[bot] 1b1b160c04 chore(deps): bump docker/build-push-action from 5.1.0 to 6.9.0 (#6004)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.1.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.1.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-16 18:30:44 +05:30
sriram veeraghanta 4149e84e62 Create dependabot.yml (#6002) 2024-11-16 18:25:29 +05:30
Aaryan Khandelwal 9408e92e44 Revert "[WEB-1435] dev: conflict free issue descriptions (#5912)" (#6000)
This reverts commit e9680cab74.
2024-11-15 17:13:31 +05:30
Aaryan Khandelwal e9680cab74 [WEB-1435] dev: conflict free issue descriptions (#5912)
* chore: new description binary endpoints

* chore: conflict free issue description

* chore: fix submitting status

* chore: update yjs utils

* chore: handle component re-mounting

* chore: update buffer response type

* chore: add try catch for issue description update

* chore: update buffer response type

* chore: description binary in retrieve

* chore: update issue description hook

* chore: decode description binary

* chore: migrations fixes and cleanup

* chore: migration fixes

* fix: inbox issue description

* chore: move update operations to the issue store

* fix: merge conflicts

* chore: reverted the commit

* chore: removed the unwanted imports

* chore: remove unnecessary props

* chore: remove unused services

* chore: update live server error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2024-11-15 16:38:58 +05:30
sriram veeraghanta 229610513a fix: django instrumentation fixes 2024-11-13 21:04:16 +05:30
sriram veeraghanta f9d9c92c83 fix: opentelemetry sdk package update 2024-11-13 20:27:47 +05:30
Aaryan Khandelwal 89588d4451 fix: issue and module link validation (#5994)
* fix: issue and module link validation

* chore: removed reset logic
2024-11-13 19:47:30 +05:30
Akshita Goyal 3eb911837c fix: display property in take (#5993) 2024-11-13 18:02:24 +05:30
rahulramesha 4b50b27a74 [WEB-2442] feat: Minor Timeline view Enhancements (#5987)
* fix timeline scroll to the right in some cases

(cherry picked from commit 17043a6c7f)

* add get position based on Date

(cherry picked from commit 2fbe22d689)

* Add sticky block name to enable it to be read throughout the block regardless of scroll position

(cherry picked from commit 447af2e05a)

* Enable blocks to have a single date on the block charts

(cherry picked from commit cb055d566b)

* revert back date-range changes

* change gradient of half blocks on Timeline

* Add instance Id for Timeline Sidebar dragging to avoid enabling dropping of other drag instances

* fix timeline scrolling height
2024-11-13 15:40:37 +05:30
rahulramesha f44db89f41 [WEB-2628] fix: Sorting by estimates (#5988)
* fix estimates sorting in Front end side

* change estimate sorting keys

* - Fix estimate sorting when local db is enabled
- Fix a bug with with sorting on special fields on spreadsheet layout
- Cleanup logging

* Add logic for order by based on layout for special cases of no load

---------

Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
2024-11-13 15:38:43 +05:30
Akshita Goyal 8c3189e1be fix: intake status count (#5990) 2024-11-13 15:38:03 +05:30
sriram veeraghanta eee2145734 fix: code spliting and instance maintenance screens 2024-11-12 19:48:31 +05:30
Aaryan Khandelwal 106710f3d0 fix: custom background color for table header (#5989) 2024-11-12 15:26:57 +05:30
Anmol Singh Bhatia db8c4f92e8 chore: theme and code refactor (#5983)
* chore: added pi colors

* chore: de-dupe modal height

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2024-11-11 19:53:43 +05:30
Anmol Singh Bhatia a6cc2c93f8 chore: worklog enhancements (#5982) 2024-11-11 19:27:07 +05:30
Bavisetti Narayan 0428ea06f6 chore: filter the deleted issue assignee (#5984) 2024-11-11 19:25:38 +05:30
Aaryan Khandelwal 7082f7014d style: remove unnecessary bottom padding from the rich text editor (#5976) 2024-11-11 16:11:34 +05:30
Anmol Singh Bhatia c7c729d81b [WEB-2283] fix: create issue modal parent select ui (#5980)
* fix: create issue modal parent select ui

* chore: code refactor
2024-11-11 16:11:10 +05:30
Aaryan Khandelwal 97eb8d43d4 style: updated margins and font styles for editor (#5978)
* style: updated margins and font styles for editor

* fix: code block font size in small font

* fix: remove duplicate code
2024-11-11 16:10:47 +05:30
Anmol Singh Bhatia 1217af1d5f chore: restrict sub-issue to have different project id than parent (#5981) 2024-11-11 16:10:27 +05:30
Bavisetti Narayan 13083a77eb chore: enable intake from project settings (#5977) 2024-11-09 17:01:21 +05:30
116 changed files with 972 additions and 531 deletions
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -367,7 +367,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.0.8
uses: softprops/action-gh-release@v2.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v6.9.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
+27 -4
View File
@@ -146,7 +146,12 @@ class UserAssetsV2Endpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
]
if type not in allowed_types:
return Response(
{
@@ -317,7 +322,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
project = Project.objects.filter(id=asset.workspace_id).first()
project = Project.objects.filter(id=asset.project_id).first()
if project is None:
return
# Delete the previous cover image
@@ -387,7 +392,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
@@ -620,7 +631,13 @@ class ProjectAssetEndpoint(BaseAPIView):
)
# Check if the file type is allowed
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
if type not in allowed_types:
return Response(
{
@@ -738,6 +755,11 @@ class ProjectAssetEndpoint(BaseAPIView):
class ProjectBulkAssetEndpoint(BaseAPIView):
def save_project_cover(self, asset, project_id):
project = Project.objects.get(id=project_id)
project.cover_image_asset_id = asset.id
project.save()
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, entity_id):
asset_ids = request.data.get("asset_ids", [])
@@ -773,6 +795,7 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
assets.update(
project_id=project_id,
)
[self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(
+5 -9
View File
@@ -262,7 +262,7 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -272,7 +272,7 @@ class PageViewSet(BaseViewSet):
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
def unlock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -283,7 +283,7 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
def access(self, request, slug, project_id, pk):
access = request.data.get("access", 0)
page = Page.objects.filter(
@@ -330,7 +330,7 @@ class PageViewSet(BaseViewSet):
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
def archive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -365,7 +365,7 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
def unarchive(self, request, slug, project_id, pk):
page = Page.objects.get(
pk=pk, workspace__slug=slug, projects__id=project_id
@@ -438,7 +438,6 @@ class PageViewSet(BaseViewSet):
class PageFavoriteViewSet(BaseViewSet):
model = UserFavorite
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@@ -465,7 +464,6 @@ class PageFavoriteViewSet(BaseViewSet):
class PageLogEndpoint(BaseAPIView):
serializer_class = PageLogSerializer
model = PageLog
@@ -504,7 +502,6 @@ class PageLogEndpoint(BaseAPIView):
class SubPagesEndpoint(BaseAPIView):
@method_decorator(gzip_page)
def get(self, request, slug, project_id, page_id):
pages = (
@@ -522,7 +519,6 @@ class SubPagesEndpoint(BaseAPIView):
class PagesDescriptionViewSet(BaseViewSet):
@allow_permission(
[
ROLE.ADMIN,
+8 -2
View File
@@ -429,6 +429,9 @@ class ProjectViewSet(BaseViewSet):
)
workspace = Workspace.objects.get(slug=slug)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
@@ -442,14 +445,17 @@ class ProjectViewSet(BaseViewSet):
serializer = ProjectSerializer(
project,
data={**request.data},
data={
**request.data,
"intake_view": intake_view,
},
context={"workspace_id": workspace.id},
partial=True,
)
if serializer.is_valid():
serializer.save()
if serializer.data["intake_view"] or request.data.get("inbox_view", False):
if intake_view:
intake = Intake.objects.filter(
project=project,
is_default=True,
-17
View File
@@ -32,7 +32,6 @@ from plane.db.models import (
Session,
)
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
from plane.bgtasks.user_deactivation_email_task import user_deactivation_email
@@ -49,7 +48,6 @@ class UserEndpoint(BaseViewSet):
def get_object(self):
return self.request.user
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve(self, request):
@@ -59,14 +57,12 @@ class UserEndpoint(BaseViewSet):
status=status.HTTP_200_OK,
)
@cache_response(60 * 60)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def retrieve_user_settings(self, request):
serialized_data = UserMeSettingsSerializer(request.user).data
return Response(serialized_data, status=status.HTTP_200_OK)
@cache_response(60 * 60)
def retrieve_instance_admin(self, request):
instance = Instance.objects.first()
is_admin = InstanceAdmin.objects.filter(
@@ -76,19 +72,9 @@ class UserEndpoint(BaseViewSet):
{"is_instance_admin": is_admin}, status=status.HTTP_200_OK
)
@invalidate_cache(
path="/api/users/me/",
)
@invalidate_cache(
path="/api/users/me/settings/",
)
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/users/me/")
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
def deactivate(self, request):
# Check all workspace user is active
user = self.get_object()
@@ -235,7 +221,6 @@ class UserSessionEndpoint(BaseAPIView):
class UpdateUserOnBoardedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_onboarded = request.data.get("is_onboarded", False)
@@ -247,7 +232,6 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
class UpdateUserTourCompletedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/")
def patch(self, request):
profile = Profile.objects.get(user_id=request.user.id)
profile.is_tour_completed = request.data.get(
@@ -305,7 +289,6 @@ class ProfileEndpoint(BaseAPIView):
serializer = ProfileSerializer(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache("/api/users/me/settings/")
def patch(self, request):
profile = Profile.objects.get(user=request.user)
serializer = ProfileSerializer(
@@ -44,7 +44,6 @@ from plane.db.models import (
WorkspaceTheme,
)
from plane.app.permissions import ROLE, allow_permission
from plane.utils.cache import cache_response, invalidate_cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
@@ -99,9 +98,6 @@ class WorkSpaceViewSet(BaseViewSet):
.select_related("owner")
)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/instances/", user=False)
def create(self, request):
try:
serializer = WorkSpaceSerializer(data=request.data)
@@ -147,7 +143,6 @@ class WorkSpaceViewSet(BaseViewSet):
status=status.HTTP_410_GONE,
)
@cache_response(60 * 60 * 2)
@allow_permission(
[
ROLE.ADMIN,
@@ -159,8 +154,6 @@ class WorkSpaceViewSet(BaseViewSet):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/")
@allow_permission(
[
ROLE.ADMIN,
@@ -170,13 +163,6 @@ class WorkSpaceViewSet(BaseViewSet):
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(
path="/api/users/me/workspaces/", multiple=True, user=False
)
@invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False
)
@allow_permission([ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -190,7 +176,6 @@ class UserWorkSpacesEndpoint(BaseAPIView):
"owner",
]
@cache_response(60 * 60 * 2)
@method_decorator(cache_control(private=True, max_age=12))
@method_decorator(vary_on_cookie)
def get(self, request):
+1 -18
View File
@@ -40,7 +40,7 @@ from plane.db.models import (
WorkspaceMember,
DraftIssue,
)
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.cache import invalidate_cache
from .. import BaseViewSet
@@ -66,7 +66,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
@cache_response(60 * 60 * 2)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
@@ -93,12 +92,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
@@ -127,16 +120,6 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_cache(
path="/api/workspaces/:slug/members/",
url_params=True,
user=False,
multiple=True,
)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
@invalidate_cache(
path="/api/users/me/workspaces/", user=False, multiple=True
)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
@@ -0,0 +1,23 @@
# Third party imports
from celery import shared_task
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import Page
from plane.utils.exception_logger import log_exception
@shared_task
def delete_pages_from_trash():
try:
# Get all the pages whose archived_at is not null, i.e., they are in the trash
Page.objects.filter(
archived_at__isnull=False,
archived_at__lte=timezone.now() - timezone.timedelta(days=90),
).delete()
return
except Exception as e:
log_exception(e)
return
+5 -1
View File
@@ -40,9 +40,13 @@ app.conf.beat_schedule = {
"task": "plane.bgtasks.api_logs_task.delete_api_logs",
"schedule": crontab(hour=0, minute=0),
},
"check-every-day-to-delete-pages-from-trash": {
"task": "plane.bgtasks.delete_pages_from_trash.delete_pages_from_trash",
"schedule": crontab(hour=0, minute=0),
},
"run-every-6-hours-for-instance-trace": {
"task": "plane.license.bgtasks.tracer.instance_traces",
"schedule": crontab(hour="*/6"),
"schedule": crontab(hour="*/6", minute=0),
},
}
@@ -1,9 +1,7 @@
# Generated by Django 4.2.15 on 2024-11-06 08:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
@@ -11,10 +11,7 @@ class InstanceSerializer(BaseSerializer):
class Meta:
model = Instance
exclude = [
"license_key",
"user_count"
]
fields = "__all__"
read_only_fields = [
"id",
"email",
+11
View File
@@ -52,6 +52,17 @@ def instance_traces():
span.set_attribute(
"is_telemetry_enabled", instance.is_telemetry_enabled
)
span.set_attribute(
"is_support_required", instance.is_support_required
)
span.set_attribute("is_setup_done", instance.is_setup_done)
span.set_attribute(
"is_signup_screen_visited", instance.is_signup_screen_visited
)
span.set_attribute("is_verified", instance.is_verified)
span.set_attribute("edition", instance.edition)
span.set_attribute("domain", instance.domain)
span.set_attribute("is_test", instance.is_test)
span.set_attribute("user_count", user_count)
span.set_attribute("workspace_count", workspace_count)
span.set_attribute("project_count", project_count)
@@ -1,6 +1,7 @@
# Python imports
import json
import secrets
import os
# Django imports
from django.core.management.base import BaseCommand, CommandError
@@ -8,10 +9,7 @@ from django.utils import timezone
from django.conf import settings
# Module imports
from plane.license.models import Instance
from plane.db.models import (
User,
)
from plane.license.models import Instance, InstanceEdition
from plane.license.bgtasks.tracer import instance_traces
@@ -32,7 +30,6 @@ class Command(BaseCommand):
payload = {
"instance_key": settings.INSTANCE_KEY,
"version": data.get("version", 0.1),
"user_count": User.objects.filter(is_bot=False).count(),
}
return payload
@@ -54,11 +51,11 @@ class Command(BaseCommand):
instance = Instance.objects.create(
instance_name="Plane Community Edition",
instance_id=secrets.token_hex(12),
license_key=None,
current_version=payload.get("version"),
latest_version=payload.get("version"),
last_checked_at=timezone.now(),
user_count=payload.get("user_count", 0),
is_test=os.environ.get("IS_TEST", "0") == "1",
edition=InstanceEdition.PLANE_COMMUNITY.value,
)
self.stdout.write(self.style.SUCCESS("Instance registered"))
@@ -69,9 +66,10 @@ class Command(BaseCommand):
payload = self.read_package_json()
# Update the instance details
instance.last_checked_at = timezone.now()
instance.user_count = payload.get("user_count", 0)
instance.current_version = payload.get("version")
instance.latest_version = payload.get("version")
instance.is_test = os.environ.get("IS_TEST", "0") == "1"
instance.edition = InstanceEdition.PLANE_COMMUNITY.value
instance.save()
# Call the instance traces task
@@ -0,0 +1,35 @@
# Generated by Django 4.2.15 on 2024-11-19 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("license", "0004_changelog_deleted_at_instance_deleted_at_and_more"),
]
operations = [
migrations.RenameField(
model_name="instance",
old_name="product",
new_name="edition",
),
migrations.RemoveField(
model_name="instance",
name="license_key",
),
migrations.RemoveField(
model_name="instance",
name="user_count",
),
migrations.AddField(
model_name="instance",
name="is_test",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="instance",
name="edition",
field=models.CharField(default="PLANE_COMMUNITY", max_length=255),
),
]
+6 -1
View File
@@ -1 +1,6 @@
from .instance import Instance, InstanceAdmin, InstanceConfiguration
from .instance import (
Instance,
InstanceAdmin,
InstanceConfiguration,
InstanceEdition,
)
+5 -7
View File
@@ -11,8 +11,8 @@ from plane.db.models import BaseModel
ROLE_CHOICES = ((20, "Admin"),)
class ProductTypes(Enum):
PLANE_CE = "plane-ce"
class InstanceEdition(Enum):
PLANE_COMMUNITY = "PLANE_COMMUNITY"
class Instance(BaseModel):
@@ -20,11 +20,10 @@ class Instance(BaseModel):
instance_name = models.CharField(max_length=255)
whitelist_emails = models.TextField(blank=True, null=True)
instance_id = models.CharField(max_length=255, unique=True)
license_key = models.CharField(max_length=256, null=True, blank=True)
current_version = models.CharField(max_length=255)
latest_version = models.CharField(max_length=255, null=True, blank=True)
product = models.CharField(
max_length=255, default=ProductTypes.PLANE_CE.value
edition = models.CharField(
max_length=255, default=InstanceEdition.PLANE_COMMUNITY.value
)
domain = models.TextField(blank=True)
# Instance specifics
@@ -37,9 +36,8 @@ class Instance(BaseModel):
is_setup_done = models.BooleanField(default=False)
# signup screen
is_signup_screen_visited = models.BooleanField(default=False)
# users
user_count = models.PositiveBigIntegerField(default=0)
is_verified = models.BooleanField(default=False)
is_test = models.BooleanField(default=False)
class Meta:
verbose_name = "Instance"
+3 -2
View File
@@ -35,8 +35,6 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = int(os.environ.get("DEBUG", "0"))
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Configure the tracer provider
service_name = os.environ.get("SERVICE_NAME", "plane-ce-api")
resource = Resource.create({"service.name": service_name})
@@ -46,6 +44,8 @@ otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://telemetry.plane.so")
otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Initialize Django instrumentation
DjangoInstrumentor().instrument()
# Allowed Hosts
@@ -303,6 +303,7 @@ CELERY_IMPORTS = (
"plane.bgtasks.email_notification_task",
"plane.bgtasks.api_logs_task",
"plane.license.bgtasks.tracer",
"plane.bgtasks.delete_pages_from_trash",
# management tasks
"plane.bgtasks.dummy_data_task",
)
-1
View File
@@ -3,7 +3,6 @@ from django.urls import path
from plane.space.views import (
IntakeIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
+1
View File
@@ -233,6 +233,7 @@ def filter_assignees(params, issue_filter, method, prefix=""):
and params.get("assignees") != "null"
):
issue_filter[f"{prefix}assignees__in"] = params.get("assignees")
issue_filter[f"{prefix}issue_assignee__deleted_at__isnull"] = True
return issue_filter
+4 -4
View File
@@ -63,7 +63,7 @@ pytz==2024.1
# jwt
PyJWT==2.8.0
# OpenTelemetry
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-instrumentation-django==0.48b0
opentelemetry-exporter-otlp==1.27.0
opentelemetry-api==1.28.1
opentelemetry-sdk==1.28.1
opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
+1 -1
View File
@@ -22,7 +22,7 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.1.1"
"turbo": "^2.3.0"
},
"packageManager": "yarn@1.22.22",
"name": "plane"
@@ -46,7 +46,7 @@ export const CoreEditorExtensionsWithoutProps = [
CustomQuoteExtension,
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomLinkExtension.configure({
@@ -78,7 +78,7 @@ export const CoreEditorExtensions = (args: TArguments) => {
DropHandlerExtension(),
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomKeymap,
@@ -67,7 +67,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
CustomQuoteExtension,
CustomHorizontalRule.configure({
HTMLAttributes: {
class: "my-4 border-custom-border-400",
class: "py-4 border-custom-border-400",
},
}),
CustomLinkExtension.configure({
+40 -30
View File
@@ -12,7 +12,7 @@
cursor: text;
font-family: var(--font-style);
font-size: var(--font-size-regular);
line-height: 1.2;
font-weight: 400;
color: inherit;
-moz-box-sizing: border-box;
box-sizing: border-box;
@@ -248,11 +248,6 @@ div[data-type="horizontalRule"] {
}
}
/* image resizer */
.moveable-control-box {
z-index: 10 !important;
}
/* Cursor styles for the inline code blocks */
@keyframes blink {
49% {
@@ -314,13 +309,23 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
}
/* end numbered, bulleted and to-do lists spacing */
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0 !important;
}
/* tailwind typography */
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 2rem;
padding-top: 28px;
}
margin-bottom: 4px;
padding-bottom: 4px;
font-size: var(--font-size-h1);
line-height: var(--line-height-h1);
font-weight: 600;
@@ -328,10 +333,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1.4rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h2);
line-height: var(--line-height-h2);
font-weight: 600;
@@ -339,10 +344,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h3);
line-height: var(--line-height-h3);
font-weight: 600;
@@ -350,10 +355,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 28px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h4);
line-height: var(--line-height-h4);
font-weight: 600;
@@ -361,10 +366,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 20px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h5);
line-height: var(--line-height-h5);
font-weight: 600;
@@ -372,30 +377,40 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 1rem;
padding-top: 20px;
}
margin-bottom: 1px;
padding-bottom: 4px;
font-size: var(--font-size-h6);
line-height: var(--line-height-h6);
font-weight: 600;
}
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
&:not(:first-child) {
margin-top: 0.25rem;
}
&:first-child {
margin-top: 0;
padding-top: 0;
}
&:not(:first-child) {
padding-top: 4px;
}
&:last-child {
padding-bottom: 4px;
}
&:not(:last-child) {
padding-bottom: 8px;
}
margin-bottom: 1px;
padding: 3px 0;
font-size: var(--font-size-regular);
line-height: var(--line-height-regular);
}
p + p {
padding-top: 8px !important;
}
.prose :where(ol):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p,
.prose :where(ul):not(:where([class~="not-prose"], [class~="not-prose"] *)) li p {
font-size: var(--font-size-list);
@@ -432,11 +447,6 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
[data-text-color="purple"] {
color: var(--editor-colors-purple-text);
}
/* [data-text-color="pink-blue-gradient"] {
background-clip: text;
color: transparent;
background-image: linear-gradient(90deg, #a961cd 50%, #e75962 100%);
} */
/* end text colors */
/* background colors */
+12 -4
View File
@@ -27,10 +27,18 @@
}
}
.table-wrapper table th {
font-weight: 500;
text-align: left;
background-color: rgba(var(--color-background-90));
.table-wrapper table {
th {
font-weight: 500;
text-align: left;
}
tr[background="none"],
tr:not([background]) {
th {
background-color: rgba(var(--color-background-90));
}
}
}
.table-wrapper table .selectedCell {
+4 -4
View File
@@ -46,8 +46,8 @@
--font-size-h5: 1.125rem;
--font-size-h6: 1rem;
--font-size-regular: 1rem;
--font-size-code: 0.85rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 2.25rem;
--line-height-h2: 2rem;
@@ -56,8 +56,8 @@
--line-height-h5: 1.5rem;
--line-height-h6: 1.5rem;
--line-height-regular: 1.5rem;
--line-height-code: 1.5rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
&.small-font {
--font-size-h1: 1.4rem;
@@ -67,8 +67,8 @@
--font-size-h5: 0.9rem;
--font-size-h6: 0.8rem;
--font-size-regular: 0.8rem;
--font-size-code: 0.8rem;
--font-size-list: var(--font-size-regular);
--font-size-code: var(--font-size-regular);
--line-height-h1: 1.8rem;
--line-height-h2: 1.6rem;
@@ -77,8 +77,8 @@
--line-height-h5: 1.2rem;
--line-height-h6: 1.2rem;
--line-height-regular: 1.2rem;
--line-height-code: 1.2rem;
--line-height-list: var(--line-height-regular);
--line-height-code: var(--line-height-regular);
}
/* end font sizes and line heights */
+1 -1
View File
@@ -23,7 +23,7 @@ export type TPage = {
};
// page filters
export type TPageNavigationTabs = "public" | "private" | "archived";
export type TPageNavigationTabs = "public" | "private" | "trash";
export type TPageFiltersSortKey =
| "name"
+5 -1
View File
@@ -18,7 +18,11 @@ export interface IProject {
close_in: number;
created_at: Date;
created_by: string;
cover_image_url: string;
// only for uploading the cover image
cover_image_asset?: null;
cover_image?: string;
// only for rendering the cover image
cover_image_url: readonly string;
cycle_view: boolean;
issue_views_view: boolean;
module_view: boolean;
+13 -4
View File
@@ -3,7 +3,6 @@ import { TUserPermissions } from "./enums";
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
export interface IUserLite {
avatar_url: string;
display_name: string;
@@ -14,7 +13,11 @@ export interface IUserLite {
last_name: string;
}
export interface IUser extends IUserLite {
cover_image_url: string | null;
// only for uploading the cover image
cover_image_asset?: string | null;
cover_image?: string | null;
// only for rendering the cover image
cover_image_url: readonly (string | null);
date_joined: string;
email: string;
is_active: boolean;
@@ -90,7 +93,6 @@ export interface IUserTheme {
sidebarBackground: string | undefined;
}
export interface IUserMemberLite extends IUserLite {
email?: string;
}
@@ -153,7 +155,14 @@ export interface IUserProfileProjectSegregation {
id: string;
pending_issues: number;
}[];
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
user_data: Pick<
IUser,
| "avatar_url"
| "cover_image_url"
| "display_name"
| "first_name"
| "last_name"
> & {
date_joined: Date;
user_timezone: string;
};
+2 -2
View File
@@ -40,8 +40,8 @@ export type TIssueOrderByOptions =
| "-issue_cycle__cycle__name"
| "target_date"
| "-target_date"
| "estimate_point"
| "-estimate_point"
| "estimate_point__key"
| "-estimate_point__key"
| "start_date"
| "-start_date"
| "link_count"
+38
View File
@@ -101,6 +101,19 @@
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
--color-sidebar-shadow-4xl: var(--color-shadow-4xl);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"],
@@ -110,6 +123,19 @@
--color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */
--color-background-80: 232, 232, 232; /* tertiary bg */
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="light"] {
@@ -200,6 +226,18 @@
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
/* pi */
--color-pi-50: var(--color-background-90);
--color-pi-100: var(--color-background-90);
--color-pi-200: var(--color-primary-200);
--color-pi-300: var(--color-primary-200);
--color-pi-400: var(--color-primary-200);
--color-pi-500: var(--color-primary-200);
--color-pi-600: 151, 150, 246;
--color-pi-700: var(--color-primary-100);
--color-pi-800: 57, 56, 149;
--color-pi-900: 30, 29, 78;
--color-pi-950: 14, 14, 37;
}
[data-theme="dark"] {
@@ -1,6 +1,6 @@
"use client";
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
@@ -62,6 +62,11 @@ const WorkspaceDashboardPage = observer(() => {
workspace_slug && project_id && is_inbox_issue ? () => fetchUserProjectInfo(workspace_slug, project_id) : null
);
const embedRemoveCurrentNotification = useCallback(
() => setCurrentSelectedNotificationId(undefined),
[setCurrentSelectedNotificationId]
);
// clearing up the selected notifications when unmounting the page
useEffect(
() => () => {
@@ -95,15 +100,12 @@ const WorkspaceDashboardPage = observer(() => {
projectId={project_id}
inboxIssueId={issue_id}
isNotificationEmbed
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
embedRemoveCurrentNotification={embedRemoveCurrentNotification}
/>
)}
</>
) : (
<IssuePeekOverview
embedIssue
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
/>
<IssuePeekOverview embedIssue embedRemoveCurrentNotification={embedRemoveCurrentNotification} />
)}
</>
)}
@@ -20,7 +20,7 @@ const PageDetailsPage = observer(() => {
const { workspaceSlug, projectId, pageId } = useParams();
// store hooks
const { getPageById } = useProjectPages();
const { fetchPageById } = useProjectPages();
const page = usePage(pageId?.toString() ?? "");
const { id, name } = page;
@@ -28,7 +28,7 @@ const PageDetailsPage = observer(() => {
const { error: pageDetailsError } = useSWR(
workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null,
workspaceSlug && projectId && pageId
? () => getPageById(workspaceSlug?.toString(), projectId?.toString(), pageId.toString())
? () => fetchPageById(workspaceSlug?.toString(), projectId?.toString(), pageId.toString())
: null,
{
revalidateIfStale: false,
@@ -1,7 +1,8 @@
"use client";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
import { useParams, useSearchParams, useRouter } from "next/navigation";
// types
import { TPageNavigationTabs } from "@plane/types";
// components
@@ -12,14 +13,18 @@ import { PagesListRoot, PagesListView } from "@/components/pages";
import { EmptyStateType } from "@/constants/empty-state";
// hooks
import { useProject } from "@/hooks/store";
import { useQueryParams } from "@/hooks/use-query-params";
const ProjectPagesPage = observer(() => {
// router
const router = useRouter();
// params
const { workspaceSlug, projectId } = useParams();
const searchParams = useSearchParams();
const type = searchParams.get("type");
const { workspaceSlug, projectId } = useParams();
// store hooks
const { getProjectById, currentProjectDetails } = useProject();
const { updateQueryParams } = useQueryParams();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Pages` : undefined;
@@ -27,9 +32,21 @@ const ProjectPagesPage = observer(() => {
const currentPageType = (): TPageNavigationTabs => {
const pageType = type?.toString();
if (pageType === "private") return "private";
if (pageType === "archived") return "archived";
if (pageType === "trash") return "trash";
return "public";
};
// update the route to public pages if the type is invalid
useEffect(() => {
const pageType = type?.toString();
if (pageType !== "public" && pageType !== "private" && pageType !== "trash") {
const updatedRoute = updateQueryParams({
paramsToAdd: {
type: "public",
},
});
router.push(updatedRoute);
}
}, [router, type, updateQueryParams]);
if (!workspaceSlug || !projectId) return <></>;
+5 -1
View File
@@ -70,11 +70,15 @@ const ProfileSettingsPage = observer(() => {
first_name: formData.first_name,
last_name: formData.last_name,
avatar_url: formData.avatar_url,
cover_image_url: formData.cover_image_url,
role: formData.role,
display_name: formData?.display_name,
user_timezone: formData.user_timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false));
setPromiseToast(updateCurrentUserDetail, {
+1
View File
@@ -0,0 +1 @@
export * from "./maintenance-message";
@@ -0,0 +1,6 @@
export const MaintenanceMessage = () => (
<h1 className="text-xl font-medium text-custom-text-100 text-center md:text-left">
Plane didn&apos;t start up. This could be because one or more Plane services failed to start. <br /> Choose View
Logs from setup.sh and Docker logs to be sure.
</h1>
);
-5
View File
@@ -1,5 +0,0 @@
"use client";
import { FC, Fragment } from "react";
export const MaintenanceMode: FC = () => <Fragment />;
@@ -74,6 +74,11 @@ export const CreateProjectForm: FC<TCreateProjectFormProps> = observer((props) =
// Upper case identifier
formData.identifier = formData.identifier?.toUpperCase();
const coverImage = formData.cover_image_url;
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (coverImage?.startsWith("http")) {
formData.cover_image = coverImage;
formData.cover_image_asset = null;
}
return createProject(workspaceSlug.toString(), formData)
.then(async (res) => {
+16 -2
View File
@@ -5,7 +5,11 @@ import { computedFn } from "mobx-utils";
// components
import { ChartDataType, IBlockUpdateDependencyData, IGanttBlock, TGanttViews } from "@/components/gantt-chart";
import { currentViewDataWithView } from "@/components/gantt-chart/data";
import { getDateFromPositionOnGantt, getItemPositionWidth } from "@/components/gantt-chart/views/helpers";
import {
getDateFromPositionOnGantt,
getItemPositionWidth,
getPositionFromDate,
} from "@/components/gantt-chart/views/helpers";
// helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// store
@@ -47,6 +51,7 @@ export interface IBaseTimelineStore {
initGantt: () => void;
getDateFromPositionOnGantt: (position: number, offsetDays: number) => Date | undefined;
getPositionFromDateOnGantt: (date: string | Date, offSetWidth: number) => number | undefined;
}
export class BaseTimeLineStore implements IBaseTimelineStore {
@@ -186,7 +191,7 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
start_date: blockData?.start_date ?? undefined,
target_date: blockData?.target_date ?? undefined,
};
if (this.currentViewData && this.currentViewData?.data?.startDate && this.currentViewData?.data?.dayWidth) {
if (this.currentViewData && (this.currentViewData?.data?.startDate || this.currentViewData?.data?.dayWidth)) {
block.position = getItemPositionWidth(this.currentViewData, block);
}
@@ -227,6 +232,15 @@ export class BaseTimeLineStore implements IBaseTimelineStore {
return Math.round(position / this.currentViewData.data.dayWidth);
};
/**
* returns position of the date on chart
*/
getPositionFromDateOnGantt = computedFn((date: string | Date, offSetWidth: number) => {
if (!this.currentViewData) return;
return getPositionFromDate(this.currentViewData, date, offSetWidth);
});
/**
* returns the date at which the position corresponds to on the timeline chart
*/
@@ -54,7 +54,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
suggestions: mentionSuggestions,
}}
{...rest}
containerClassName={cn("relative pl-3 pb-3", containerClassName)}
containerClassName={cn("relative pl-3", containerClassName)}
/>
);
});
@@ -68,7 +68,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
const isBlockVisibleOnChart = block.start_date && block.target_date;
const isBlockVisibleOnChart = block.start_date || block.target_date;
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
const isBlockHoveredOn = isBlockActive(block.id);
@@ -46,10 +46,11 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
const { isMoving, handleBlockDrag } = useGanttResizable(block, resizableRef, ganttContainerRef, updateBlockDates);
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!block || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
const isBlockComplete = block?.start_date && block?.target_date;
const isBlockVisibleOnChart = block.start_date && block.target_date;
// hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!block || (!showAllBlocks && !isBlockVisibleOnChart)) return null;
if (!block.data) return null;
@@ -63,7 +64,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
ref={resizableRef}
style={{
height: `${BLOCK_HEIGHT}px`,
transform: `translateX(${block.position?.marginLeft}px)`,
marginLeft: `${block.position?.marginLeft}px`,
width: `${block.position?.width}px`,
}}
>
@@ -88,7 +89,7 @@ export const GanttChartBlock: React.FC<Props> = observer((props) => {
handleBlockDrag={handleBlockDrag}
enableBlockLeftResize={enableBlockLeftResize}
enableBlockRightResize={enableBlockRightResize}
enableBlockMove={enableBlockMove}
enableBlockMove={enableBlockMove && !!isBlockComplete}
isMoving={isMoving}
ganttContainerRef={ganttContainerRef}
/>
@@ -28,7 +28,7 @@ import { IssueBulkOperationsRoot } from "@/plane-web/components/issues";
import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-status";
//
import { GanttChartRowList } from "../blocks/block-row-list";
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
import { DEFAULT_BLOCK_WIDTH, GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
import { getItemPositionWidth } from "../views";
import { TimelineDragHelper } from "./timeline-drag-helper";
@@ -108,14 +108,20 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const approxRangeLeft = scrollLeft;
const approxRangeRight = scrollWidth - (scrollLeft + clientWidth);
const calculatedRangeRight = itemsContainerWidth - (scrollLeft + clientWidth);
if (approxRangeRight < clientWidth) updateCurrentViewRenderPayload("right", currentView);
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
if (approxRangeRight < clientWidth || calculatedRangeRight < clientWidth) {
updateCurrentViewRenderPayload("right", currentView);
}
if (approxRangeLeft < clientWidth) {
updateCurrentViewRenderPayload("left", currentView);
}
};
const handleScrollToBlock = (block: IGanttBlock) => {
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
const scrollToDate = getDate(block.start_date);
const scrollToEndDate = !block.start_date && block.target_date;
const scrollToDate = block.start_date ? getDate(block.start_date) : getDate(block.target_date);
let chartData;
if (!scrollContainer || !currentViewData || !scrollToDate) return;
@@ -129,7 +135,8 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
setTimeout(() => {
if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4;
if (updatedPosition)
scrollContainer.scrollLeft = updatedPosition.marginLeft - 4 - (scrollToEndDate ? DEFAULT_BLOCK_WIDTH : 0);
});
};
@@ -189,6 +196,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
style={{
width: `${itemsContainerWidth}px`,
transform: `translateY(${HEADER_HEIGHT}px)`,
paddingBottom: `${HEADER_HEIGHT}px`,
}}
>
<GanttChartRowList
@@ -25,7 +25,7 @@ export const MonthChartView: FC<any> = observer(() => {
const marginLeftDays = getNumberOfDaysBetweenTwoDates(monthsStartDate, weeksStartDate);
return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData && (
<div className="relative flex flex-col outline-[0.25px] outline outline-custom-border-200">
{/** Header Div */}
@@ -15,7 +15,7 @@ export const QuarterChartView: FC<any> = observer(() => {
const quarterBlocks: IQuarterMonthBlock[] = groupMonthsToQuarters(monthBlocks);
return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData &&
quarterBlocks?.map((quarterBlock, rootIndex) => (
<div
@@ -13,7 +13,7 @@ export const WeekChartView: FC<any> = observer(() => {
const weekBlocks: IWeekBlock[] = renderView;
return (
<div className={`absolute top-0 left-0 h-max w-max flex`} style={{ minHeight: `calc(100% + ${HEADER_HEIGHT}px` }}>
<div className={`absolute top-0 left-0 min-h-full h-max w-max flex`}>
{currentViewData &&
weekBlocks?.map((block, rootIndex) => (
<div
@@ -6,4 +6,6 @@ export const GANTT_BREADCRUMBS_HEIGHT = 40;
export const SIDEBAR_WIDTH = 360;
export const DEFAULT_BLOCK_WIDTH = 60;
export const GANTT_SELECT_GROUP = "gantt-issues";
@@ -71,7 +71,7 @@ export const useGanttResizable = (
// calculate new marginLeft and update the initial marginLeft to the newly calculated one
marginLeft = Math.round(mouseX / dayWidth) * dayWidth;
// get Dimensions from dom's style
const prevMarginLeft = parseFloat(resizableDiv.style.transform.slice(11, -3));
const prevMarginLeft = parseFloat(resizableDiv.style.marginLeft.slice(0, -2));
const prevWidth = parseFloat(resizableDiv.style.width.slice(0, -2));
// calculate new width
const marginDelta = prevMarginLeft - marginLeft;
@@ -88,7 +88,7 @@ export const useGanttResizable = (
if (width < dayWidth) return;
resizableDiv.style.width = `${width}px`;
resizableDiv.style.transform = `translateX(${marginLeft}px)`;
resizableDiv.style.marginLeft = `${marginLeft}px`;
const deltaLeft = Math.round((marginLeft - (block.position?.marginLeft ?? 0)) / dayWidth) * dayWidth;
const deltaWidth = Math.round((width - (block.position?.width ?? 0)) / dayWidth) * dayWidth;
@@ -34,7 +34,7 @@ export const GanttDnDHOC = observer((props: Props) => {
draggable({
element,
canDrag: () => isDragEnabled,
getInitialData: () => ({ id }),
getInitialData: () => ({ id, dragInstanceId: "GANTT_REORDER" }),
onDragStart: () => {
setIsDragging(true);
},
@@ -44,7 +44,7 @@ export const GanttDnDHOC = observer((props: Props) => {
}),
dropTargetForElements({
element,
canDrop: ({ source }) => source?.data?.id !== id,
canDrop: ({ source }) => source?.data?.id !== id && source?.data?.dragInstanceId === "GANTT_REORDER",
getData: ({ input, element }) => {
const data = { id };
@@ -27,8 +27,8 @@ export const IssuesSidebarBlock = observer((props: Props) => {
const { updateActiveBlockId, isBlockActive, getNumberOfDaysFromPosition } = useTimeLineChartStore();
const { getIsIssuePeeked } = useIssueDetail();
const isBlockVisibleOnChart = !!block?.start_date && !!block?.target_date;
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
const isBlockComplete = !!block?.start_date && !!block?.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
if (!block?.data) return null;
@@ -22,8 +22,8 @@ export const ModulesSidebarBlock: React.FC<Props> = observer((props) => {
if (!block) return <></>;
const isBlockVisibleOnChart = !!block.start_date && !!block.target_date;
const duration = isBlockVisibleOnChart ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
const isBlockComplete = !!block.start_date && !!block.target_date;
const duration = isBlockComplete ? getNumberOfDaysFromPosition(block?.position?.width) : undefined;
return (
<div
@@ -1,8 +1,6 @@
import { addDaysToDate, findTotalDaysInRange, getDate } from "@/helpers/date-time.helper";
import { DEFAULT_BLOCK_WIDTH } from "../constants";
import { ChartDataType, IGanttBlock } from "../types";
import { IMonthBlock, IMonthView, monthView } from "./month-view";
import { quarterView } from "./quarter-view";
import { IWeekBlock, weekView } from "./week-view";
/**
* Generates Date by using Day, month and Year
@@ -84,7 +82,7 @@ export const getDateFromPositionOnGantt = (position: number, chartData: ChartDat
*/
export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttBlock) => {
let scrollPosition: number = 0;
let scrollWidth: number = 0;
let scrollWidth: number = DEFAULT_BLOCK_WIDTH;
const { startDate: chartStartDate } = chartData.data;
const { start_date, target_date } = itemData;
@@ -92,24 +90,42 @@ export const getItemPositionWidth = (chartData: ChartDataType, itemData: IGanttB
const itemStartDate = getDate(start_date);
const itemTargetDate = getDate(target_date);
if (!itemStartDate || !itemTargetDate) return;
chartStartDate.setHours(0, 0, 0, 0);
itemStartDate.setHours(0, 0, 0, 0);
itemTargetDate.setHours(0, 0, 0, 0);
itemStartDate?.setHours(0, 0, 0, 0);
itemTargetDate?.setHours(0, 0, 0, 0);
// get number of days from chart start date to block's start date
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, itemStartDate, false) ?? 0);
if (!positionDaysDifference) return;
if (!itemStartDate && !itemTargetDate) return;
// get scroll position from the number of days and width of each day
scrollPosition = positionDaysDifference * chartData.data.dayWidth;
scrollPosition = itemStartDate
? getPositionFromDate(chartData, itemStartDate, 0)
: getPositionFromDate(chartData, itemTargetDate!, -1 * DEFAULT_BLOCK_WIDTH);
// get width of block
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
if (itemStartDate && itemTargetDate) {
// get width of block
const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime();
const widthDaysDifference: number = Math.abs(Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)));
scrollWidth = (widthDaysDifference + 1) * chartData.data.dayWidth;
}
return { marginLeft: scrollPosition, width: scrollWidth };
};
export const getPositionFromDate = (chartData: ChartDataType, date: string | Date, offsetWidth: number) => {
const currDate = getDate(date);
const { startDate: chartStartDate } = chartData.data;
if (!currDate || !chartStartDate) return 0;
chartStartDate.setHours(0, 0, 0, 0);
currDate.setHours(0, 0, 0, 0);
// get number of days from chart start date to block's start date
const positionDaysDifference = Math.round(findTotalDaysInRange(chartStartDate, currDate, false) ?? 0);
if (!positionDaysDifference) return 0;
// get scroll position from the number of days and width of each day
return positionDaysDifference * chartData.data.dayWidth + offsetWidth;
};
@@ -286,7 +286,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
{shouldRenderDuplicateModal && (
<div
ref={modalContainerRef}
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
className="relative flex flex-col gap-2.5 px-3 py-4 rounded-lg shadow-xl bg-pi-50"
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
>
<DuplicateModalRoot
@@ -91,7 +91,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
{/* labels */}
<div className="h-7">
<IssueLabelSelect
setIsOpen={() => { }}
setIsOpen={() => {}}
value={data?.label_ids || []}
onChange={(labelIds) => handleData("label_ids", labelIds)}
projectId={projectId}
@@ -171,13 +171,13 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
{/* add parent */}
{isVisible && (
<>
<div className="h-7">
{selectedParentIssue ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">
@@ -188,6 +188,8 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
</button>
}
placement="bottom-start"
className="h-full w-full"
customButtonClassName="h-full"
tabIndex={getIndex("parent_id")}
>
<>
@@ -208,7 +210,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
@@ -226,7 +228,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
projectId={projectId}
issueId={undefined}
/>
</>
</div>
)}
</div>
);
+1
View File
@@ -1 +1,2 @@
export * from "./not-ready-view";
export * from "./maintenance-view";
@@ -0,0 +1,34 @@
"use client";
import { FC } from "react";
import Image from "next/image";
// ui
import { Button } from "@plane/ui";
// layouts
import DefaultLayout from "@/layouts/default-layout";
// components
import { MaintenanceMessage } from "@/plane-web/components/instance";
// images
import maintenanceModeImage from "@/public/maintenance-mode.webp";
export const MaintenanceView: FC = () => (
<DefaultLayout>
<div className="relative container mx-auto h-full w-full flex flex-col md:flex-row gap-2 items-center justify-center gap-y-5 bg-custom-background-100 text-center">
<div className="relative w-full">
<Image
src={maintenanceModeImage}
height="176"
width="288"
alt="ProjectSettingImg"
className="w-full h-full object-fill object-center"
/>
</div>
<div className="w-full space-y-4 relative flex flex-col justify-center md:justify-start items-center md:items-start">
<MaintenanceMessage />
<Button variant="outline-primary" onClick={() => window.location.reload()}>
Reload
</Button>
</div>
</div>
</DefaultLayout>
);
@@ -151,6 +151,7 @@ export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
data={createUpdateModalData}
onClose={handleCreateUpdateModalClose}
onSubmit={handleCreateUpdateModalOnSubmit}
isProjectSelectionDisabled
/>
)}
@@ -162,7 +163,6 @@ export const IssueDetailWidgetModals: FC<Props> = observer((props) => {
handleClose={handleExistingIssuesModalClose}
searchParams={existingIssuesModalSearchParams}
handleOnSubmit={handleExistingIssuesModalOnSubmit}
workspaceLevelToggle
/>
)}
@@ -27,6 +27,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
type: TOAST_TYPE.ERROR,
title: "Link not created",
});
throw error;
}
},
update: async (linkId: string, data: Partial<TIssueLink>) => {
@@ -44,6 +45,7 @@ export const useLinkOperations = (workspaceSlug: string, projectId: string, issu
type: TOAST_TYPE.ERROR,
title: "Link not updated",
});
throw error;
}
},
remove: async (linkId: string) => {
@@ -11,7 +11,7 @@ import { TOAST_TYPE, setToast } from "@plane/ui";
import { IssueCommentCreate } from "@/components/issues";
import { IssueActivityCommentRoot } from "@/components/issues/issue-detail";
// hooks
import { useIssueDetail, useProject, useUserPermissions } from "@/hooks/store";
import { useIssueDetail, useProject, useUser, useUserPermissions } from "@/hooks/store";
// plane web components
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
// plane web constants
@@ -38,15 +38,25 @@ export type TActivityOperations = {
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false, isIntakeIssue = false } = props;
// hooks
const { createComment, updateComment, removeComment } = useIssueDetail();
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getProjectById } = useProject();
//derived values
const isGuest = (projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId) ?? EUserPermissions.GUEST) === EUserPermissions.GUEST;
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest;
// state
const [selectedFilters, setSelectedFilters] = useState<TActivityFilters[]>(defaultActivityFilters);
// hooks
const {
issue: { getIssueById },
createComment,
updateComment,
removeComment,
} = useIssueDetail();
const { projectPermissionsByWorkspaceSlugAndProjectId } = useUserPermissions();
const { getProjectById } = useProject();
const { data: currentUser } = useUser();
//derived values
const issue = issueId ? getIssueById(issueId) : undefined;
const currentUserProjectRole = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
const isAdmin = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.ADMIN;
const isGuest = (currentUserProjectRole ?? EUserPermissions.GUEST) === EUserPermissions.GUEST;
const isAssigned = issue?.assignee_ids && currentUser?.id ? issue?.assignee_ids.includes(currentUser?.id) : false;
const isWorklogButtonEnabled = !isIntakeIssue && !isGuest && (isAdmin || isAssigned);
// toggle filter
const toggleFilter = (filter: TActivityFilters) => {
setSelectedFilters((prevFilters) => {
@@ -7,8 +7,6 @@ import { Controller, useForm } from "react-hook-form";
import type { TIssueLinkEditableFields } from "@plane/types";
// plane ui
import { Button, Input, ModalCore } from "@plane/ui";
// helpers
import { checkURLValidity } from "@/helpers/string.helper";
// hooks
import { useIssueDetail } from "@/hooks/store";
// types
@@ -48,14 +46,18 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
const onClose = () => {
setIssueLinkData(null);
reset();
if (handleOnClose) handleOnClose();
};
const handleFormSubmit = async (formData: TIssueLinkCreateFormFieldOptions) => {
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: formData.url });
else await linkOperations.update(formData.id as string, { title: formData.title, url: formData.url });
onClose();
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
try {
if (!formData || !formData.id) await linkOperations.create({ title: formData.title, url: parsedUrl });
else await linkOperations.update(formData.id, { title: formData.title, url: parsedUrl });
onClose();
} catch (error) {
console.error("error", error);
}
};
useEffect(() => {
@@ -77,7 +79,6 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observe
name="url"
rules={{
required: "URL is required",
validate: (value) => checkURLValidity(value) || "URL is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
@@ -58,6 +58,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
type: TOAST_TYPE.ERROR,
title: "Link not created",
});
throw error;
}
},
update: async (linkId: string, data: Partial<TIssueLink>) => {
@@ -76,6 +77,7 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
type: TOAST_TYPE.ERROR,
title: "Link not updated",
});
throw error;
}
},
remove: async (linkId: string) => {
@@ -4,6 +4,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// ui
import { Tooltip, ControlLink } from "@plane/ui";
// components
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
@@ -13,7 +15,8 @@ import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-red
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues";
// local types
//
import { getBlockViewDetails } from "../utils";
import { GanttStoreType } from "./base-gantt-root";
type Props = {
@@ -39,36 +42,37 @@ export const IssueGanttBlock: React.FC<Props> = observer((props) => {
const stateDetails =
issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id);
const { message, blockStyle } = getBlockViewDetails(issueDetails, stateDetails?.color ?? "");
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
return (
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={{
backgroundColor: stateDetails?.color,
}}
onClick={handleIssuePeekOverview}
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
<div>{message}</div>
</div>
}
position="top-left"
disabled={!message}
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{issueDetails?.name}</h5>
<div>
{renderFormattedDate(issueDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(issueDetails?.target_date ?? "")}
</div>
</div>
}
position="top-left"
<div
id={`issue-${issueId}`}
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={blockStyle}
onClick={handleIssuePeekOverview}
>
<div className="relative w-full overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100">
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<div
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
style={{ left: `${SIDEBAR_WIDTH}px` }}
>
{issueDetails?.name}
</div>
</Tooltip>
</div>
</div>
</Tooltip>
);
});
@@ -92,7 +96,11 @@ export const IssueGanttSidebarBlock: React.FC<Props> = observer((props) => {
// derived values
const issueDetails = getIssueById(issueId);
const handleIssuePeekOverview = () => handleRedirection(workspaceSlug, issueDetails, isMobile);
const handleIssuePeekOverview = (e: any) => {
e.stopPropagation(true);
e.preventDefault();
handleRedirection(workspaceSlug, issueDetails, isMobile);
};
return (
<ControlLink
@@ -1,5 +1,6 @@
"use client";
import { CSSProperties } from "react";
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import clone from "lodash/clone";
import concat from "lodash/concat";
@@ -32,6 +33,7 @@ import { Logo } from "@/components/common";
import { ISSUE_PRIORITIES, EIssuesStoreType } from "@/constants/issue";
import { STATE_GROUPS } from "@/constants/state";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// store
import { ICycleStore } from "@/store/cycle.store";
@@ -672,3 +674,39 @@ export function getApproximateCardHeight(displayProperties: IIssueDisplayPropert
return cardHeight;
}
/**
* This Method is used to get Block view details, that returns block style and tooltip message
* @param block
* @param backgroundColor
* @returns
*/
export const getBlockViewDetails = (
block: { start_date: string | undefined | null; target_date: string | undefined | null } | undefined | null,
backgroundColor: string
) => {
const isBlockVisibleOnChart = block?.start_date || block?.target_date;
const isBlockComplete = block?.start_date && block?.target_date;
let message;
const blockStyle: CSSProperties = {
backgroundColor,
};
if (isBlockVisibleOnChart && !isBlockComplete) {
if (block?.start_date) {
message = `From ${renderFormattedDate(block.start_date)}`;
blockStyle.maskImage = `linear-gradient(to right, ${backgroundColor} 50%, transparent 95%)`;
} else if (block?.target_date) {
message = `Till ${renderFormattedDate(block.target_date)}`;
blockStyle.maskImage = `linear-gradient(to left, ${backgroundColor} 50%, transparent 95%)`;
}
} else if (isBlockComplete) {
message = `${renderFormattedDate(block?.start_date)} to ${renderFormattedDate(block?.target_date)}`;
}
return {
message,
blockStyle,
};
};
@@ -37,6 +37,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
moveToIssue = false,
modalTitle,
primaryButtonText,
isProjectSelectionDisabled = false,
} = props;
const issueStoreType = useIssueStoreType();
@@ -361,6 +362,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
moveToIssue={moveToIssue}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
) : (
<IssueFormRoot
@@ -383,6 +385,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
)}
</ModalCore>
@@ -262,58 +262,62 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
)}
/>
)}
{parentId ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
>
{selectedParentIssue?.project_id && (
<IssueIdentifier
projectId={selectedParentIssue.project_id}
issueTypeId={selectedParentIssue.type_id}
projectIdentifier={selectedParentIssue?.project__identifier}
issueSequenceId={selectedParentIssue.sequence_id}
textContainerClassName="text-xs"
/>
)}
</button>
}
placement="bottom-start"
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)}
/>
</>
</CustomMenu>
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
<div className="h-7">
{parentId ? (
<CustomMenu
customButton={
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
>
{selectedParentIssue?.project_id && (
<IssueIdentifier
projectId={selectedParentIssue.project_id}
issueTypeId={selectedParentIssue.type_id}
projectIdentifier={selectedParentIssue?.project__identifier}
issueSequenceId={selectedParentIssue.sequence_id}
textContainerClassName="text-xs"
/>
)}
</button>
}
placement="bottom-start"
className="h-full w-full"
customButtonClassName="h-full"
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
Change parent issue
</CustomMenu.MenuItem>
<Controller
control={control}
name="parent_id"
render={({ field: { onChange } }) => (
<CustomMenu.MenuItem
className="!p-1"
onClick={() => {
onChange(null);
handleFormChange();
}}
>
Remove parent issue
</CustomMenu.MenuItem>
)}
/>
</>
</CustomMenu>
) : (
<button
type="button"
className="flex cursor-pointer items-center justify-between gap-1 h-full rounded border-[0.5px] border-custom-border-300 px-2 py-0.5 text-xs hover:bg-custom-background-80"
onClick={() => setParentIssueListModalOpen(true)}
>
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
<span className="whitespace-nowrap">Add parent</span>
</button>
)}
</div>
<Controller
control={control}
name="parent_id"
@@ -38,6 +38,7 @@ export interface DraftIssueProps {
};
isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean;
}
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
@@ -58,6 +59,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
primaryButtonText,
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
// states
const [issueDiscardModal, setIssueDiscardModal] = useState(false);
@@ -179,6 +181,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
primaryButtonText={primaryButtonText}
isDuplicateModalOpen={isDuplicateModalOpen}
handleDuplicateIssueModal={handleDuplicateIssueModal}
isProjectSelectionDisabled={isProjectSelectionDisabled}
/>
</>
);
@@ -71,6 +71,7 @@ export interface IssueFormProps {
};
isDuplicateModalOpen: boolean;
handleDuplicateIssueModal: (isOpen: boolean) => void;
isProjectSelectionDisabled?: boolean;
}
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
@@ -93,6 +94,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
},
isDuplicateModalOpen,
handleDuplicateIssueModal,
isProjectSelectionDisabled = false,
} = props;
// states
@@ -336,7 +338,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
<div className="flex items-center gap-x-1">
<IssueProjectSelect
control={control}
disabled={!!data?.id || !!data?.sourceIssueId}
disabled={!!data?.id || !!data?.sourceIssueId || isProjectSelectionDisabled}
handleFormChange={handleFormChange}
/>
{projectId && (
@@ -511,7 +513,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
{shouldRenderDuplicateModal && (
<div
ref={modalContainerRef}
className="relative flex flex-col gap-2.5 h-full px-3 py-4 rounded-lg shadow-xl bg-pi-50"
className="relative flex flex-col gap-2.5 px-3 py-4 rounded-lg shadow-xl bg-pi-50"
style={{ maxHeight: formRef?.current?.offsetHeight ? `${formRef.current.offsetHeight}px` : "436px" }}
>
<DuplicateModalRoot
@@ -27,6 +27,7 @@ export interface IssuesModalProps {
default: string;
loading: string;
};
isProjectSelectionDisabled?: boolean;
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer(
@@ -135,8 +135,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<span>Created by</span>
</div>
<div className="w-full h-full flex items-center gap-1.5 rounded px-2 py-0.5 text-sm justify-between cursor-not-allowed">
<ButtonAvatars showTooltip userIds={createdByDetails?.id} />
<span className="flex-grow truncate text-xs leading-5">{createdByDetails?.display_name}</span>
<ButtonAvatars
showTooltip
userIds={createdByDetails?.display_name.includes("-intake") ? null : createdByDetails?.id}
/>
<span className="flex-grow truncate text-xs leading-5">
{createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name}
</span>
</div>
</div>
)}
@@ -3,13 +3,14 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// hooks
// ui
import { Tooltip, ModuleStatusIcon } from "@plane/ui";
// helpers
import { MODULE_STATUS } from "@/constants/module";
import { renderFormattedDate } from "@/helpers/date-time.helper";
// components
import { SIDEBAR_WIDTH } from "@/components/gantt-chart/constants";
import { getBlockViewDetails } from "@/components/issues/issue-layouts/utils";
// constants
import { MODULE_STATUS } from "@/constants/module";
// hooks
import { useModule } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -30,31 +31,40 @@ export const ModuleGanttBlock: React.FC<Props> = observer((props) => {
// hooks
const { isMobile } = usePlatformOS();
const { message, blockStyle } = getBlockViewDetails(
moduleDetails,
MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color ?? ""
);
return (
<div
className="relative flex h-full w-full items-center rounded"
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }}
onClick={() =>
router.push(`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`)
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{moduleDetails?.name}</h5>
<div>{message}</div>
</div>
}
position="top-left"
>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<Tooltip
isMobile={isMobile}
tooltipContent={
<div className="space-y-1">
<h5>{moduleDetails?.name}</h5>
<div>
{renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "}
{renderFormattedDate(moduleDetails?.target_date ?? "")}
</div>
</div>
<div
className="relative flex h-full w-full cursor-pointer items-center rounded"
style={blockStyle}
onClick={() =>
router.push(
`/${workspaceSlug?.toString()}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`
)
}
position="top-left"
>
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{moduleDetails?.name}</div>
</Tooltip>
</div>
<div className="absolute left-0 top-0 h-full w-full bg-custom-background-100/50" />
<div
className="sticky w-auto overflow-hidden truncate px-2.5 py-1 text-sm text-custom-text-100"
style={{ left: `${SIDEBAR_WIDTH}px` }}
>
{moduleDetails?.name}
</div>
</div>
</Tooltip>
);
});
@@ -6,8 +6,6 @@ import { Controller, useForm } from "react-hook-form";
import type { ILinkDetails, ModuleLink } from "@plane/types";
// plane ui
import { Button, Input, ModalCore, setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { checkURLValidity } from "@/helpers/string.helper";
type Props = {
createLink: (formData: ModuleLink) => Promise<void>;
@@ -39,9 +37,10 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
};
const handleFormSubmit = async (formData: ModuleLink) => {
const parsedUrl = formData.url.startsWith("http") ? formData.url : `http://${formData.url}`;
const payload = {
title: formData.title,
url: formData.url,
url: parsedUrl,
};
try {
@@ -92,7 +91,6 @@ export const CreateUpdateModuleLinkModal: FC<Props> = (props) => {
name="url"
rules={{
required: "URL is required",
validate: (value) => checkURLValidity(value) || "URL is invalid",
}}
render={({ field: { value, onChange, ref } }) => (
<Input
+3 -3
View File
@@ -25,9 +25,9 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
// page filters
const { fontSize } = usePageFilters();
// ui
const titleClassName = cn("bg-transparent tracking-[-2%] font-semibold", {
"text-[1.6rem] leading-[1.8rem]": fontSize === "small-font",
"text-[2rem] leading-[2.25rem]": fontSize === "large-font",
const titleClassName = cn("bg-transparent tracking-[-2%] font-bold", {
"text-[1.6rem] leading-[1.9rem]": fontSize === "small-font",
"text-[2rem] leading-[2.375rem]": fontSize === "large-font",
});
return (
@@ -22,8 +22,8 @@ const pageTabs: { key: TPageNavigationTabs; label: string }[] = [
label: "Private",
},
{
key: "archived",
label: "Archived",
key: "trash",
label: "Trash",
},
];
@@ -2,33 +2,29 @@
import { Loader } from "@plane/ui";
export const PageLoader: React.FC = (props) => {
const {} = props;
return (
<div className="relative w-full h-full flex flex-col">
<div className="px-3 border-b border-custom-border-100 py-3">
<Loader className="relative flex items-center gap-2">
<Loader.Item width="200px" height="30px" />
<div className="relative flex items-center gap-2 ml-auto">
<Loader.Item width="100px" height="30px" />
<Loader.Item width="100px" height="30px" />
export const PageLoader: React.FC = () => (
<div className="relative w-full h-full flex flex-col">
<div className="px-3 border-b border-custom-border-100 py-3">
<Loader className="relative flex items-center gap-2">
<Loader.Item width="200px" height="30px" />
<div className="relative flex items-center gap-2 ml-auto">
<Loader.Item width="100px" height="30px" />
<Loader.Item width="100px" height="30px" />
</div>
</Loader>
</div>
<div>
{Array.from(Array(10)).map((_, index) => (
<Loader key={index} className="relative flex items-center gap-2 p-3 py-4 border-b border-custom-border-100">
<Loader.Item width={`${250 + 10 * Math.floor(Math.random() * 10)}px`} height="22px" />
<div className="ml-auto relative flex items-center gap-2">
<Loader.Item width="60px" height="22px" />
<Loader.Item width="22px" height="22px" />
<Loader.Item width="22px" height="22px" />
<Loader.Item width="22px" height="22px" />
</div>
</Loader>
</div>
<div>
{Array.from(Array(10)).map((i) => (
<Loader key={i} className="relative flex items-center gap-2 p-3 py-4 border-b border-custom-border-100">
<Loader.Item width={`${250 + 10 * Math.floor(Math.random() * 10)}px`} height="22px" />
<div className="ml-auto relative flex items-center gap-2">
<Loader.Item width="60px" height="22px" />
<Loader.Item width="22px" height="22px" />
<Loader.Item width="22px" height="22px" />
<Loader.Item width="22px" height="22px" />
</div>
</Loader>
))}
</div>
))}
</div>
);
};
</div>
);
@@ -32,35 +32,35 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
if (loader === "init-loader") return <PageLoader />;
// if no pages exist in the active page type
if (!isAnyPageAvailable || pageIds?.length === 0) {
if (!isAnyPageAvailable) {
return (
<EmptyState
type={EmptyStateType.PROJECT_PAGE}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true });
}}
/>
);
}
if (pageType === "public")
return (
<EmptyState
type={EmptyStateType.PROJECT_PAGE_PUBLIC}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
}}
/>
);
if (pageType === "private")
return (
<EmptyState
type={EmptyStateType.PROJECT_PAGE_PRIVATE}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
}}
/>
);
if (pageType === "archived") return <EmptyState type={EmptyStateType.PROJECT_PAGE_ARCHIVED} />;
return (
<div className="size-full">
{!isAnyPageAvailable && (
<EmptyState
type={EmptyStateType.PROJECT_PAGE}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true });
}}
/>
)}
{pageType === "public" ? (
<EmptyState
type={EmptyStateType.PROJECT_PAGE_PUBLIC}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
}}
/>
) : pageType === "private" ? (
<EmptyState
type={EmptyStateType.PROJECT_PAGE_PRIVATE}
primaryButtonOnClick={() => {
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
}}
/>
) : pageType === "trash" ? (
<EmptyState type={EmptyStateType.PROJECT_PAGE_TRASH} />
) : null}
</div>
);
}
// if no pages match the filter criteria
if (filteredPageIds?.length === 0)
+6 -1
View File
@@ -144,10 +144,15 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
network: formData.network,
identifier: formData.identifier,
description: formData.description,
cover_image_url: formData.cover_image_url,
logo_props: formData.logo_props,
// timezone: formData.timezone,
};
// if unsplash or a pre-defined image is uploaded, delete the old uploaded asset
if (formData.cover_image_url?.startsWith("http")) {
payload.cover_image = formData.cover_image_url;
payload.cover_image_asset = null;
}
if (project.identifier !== formData.identifier)
await projectService
+2 -2
View File
@@ -79,9 +79,9 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
},
estimate: {
title: "Estimate",
ascendingOrderKey: "estimate_point",
ascendingOrderKey: "estimate_point__key",
ascendingOrderTitle: "Low",
descendingOrderKey: "-estimate_point",
descendingOrderKey: "-estimate_point__key",
descendingOrderTitle: "High",
icon: Triangle,
Column: SpreadsheetEstimateColumn,
+2 -4
View File
@@ -3,11 +3,9 @@ import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { LogoSpinner } from "@/components/common";
import { InstanceNotReady } from "@/components/instance";
import { InstanceNotReady, MaintenanceView } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store";
// plane web components
import { MaintenanceMode } from "@/plane-web/components/maintenance-mode";
type TInstanceWrapper = {
children: ReactNode;
@@ -32,7 +30,7 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
</div>
);
if (instanceSWRError) return <MaintenanceMode />;
if (instanceSWRError) return <MaintenanceView />;
// something went wrong while in the request
if (error && error?.status === "error") return <>{children}</>;
+1
View File
@@ -300,6 +300,7 @@ export class Storage {
const { cursor, group_by, sub_group_by } = queries;
const query = issueFilterQueryConstructor(this.workspaceSlug, projectId, queries);
log("#### Query", query);
const countQuery = issueFilterCountQueryConstructor(this.workspaceSlug, projectId, queries);
const start = performance.now();
let issuesRaw: any[] = [];
+7 -4
View File
@@ -87,10 +87,10 @@ export const getStates = async (workspaceSlug: string) => {
export const getEstimatePoints = async (workspaceSlug: string) => {
const estimateService = new EstimateService();
const estimates = await estimateService.fetchWorkspaceEstimates(workspaceSlug);
const objects: IEstimatePoint[] = [];
let objects: IEstimatePoint[] = [];
(estimates || []).forEach((estimate: IEstimate) => {
if (estimate?.points) {
objects.concat(estimate.points);
objects = objects.concat(estimate.points);
}
});
return objects;
@@ -104,6 +104,9 @@ export const getMembers = async (workspaceSlug: string) => {
};
export const loadWorkSpaceData = async (workspaceSlug: string) => {
if (!persistence.db || !persistence.db.exec) {
return;
}
log("Loading workspace data");
const promises = [];
promises.push(getLabels(workspaceSlug));
@@ -112,7 +115,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
promises.push(getStates(workspaceSlug));
promises.push(getEstimatePoints(workspaceSlug));
promises.push(getMembers(workspaceSlug));
const [labels, modules, cycles, states, estimates, memebers] = await Promise.all(promises);
const [labels, modules, cycles, states, estimates, members] = await Promise.all(promises);
const start = performance.now();
await persistence.db.exec("BEGIN;");
@@ -121,7 +124,7 @@ export const loadWorkSpaceData = async (workspaceSlug: string) => {
await batchInserts(cycles, "cycles", cycleSchema);
await batchInserts(states, "states", stateSchema);
await batchInserts(estimates, "estimate_points", estimatePointSchema);
await batchInserts(memebers, "members", memberSchema);
await batchInserts(members, "members", memberSchema);
await persistence.db.exec("COMMIT;");
const end = performance.now();
log("Time taken to load workspace data", end - start);
+6 -8
View File
@@ -18,6 +18,8 @@ export const SPECIAL_ORDER_BY = {
"-issue_cycle__cycle__name": "cycles",
state__name: "states",
"-state__name": "states",
estimate_point__key: "estimate_point",
"-estimate_point__key": "estimate_point",
};
export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: string, queries: any) => {
const {
@@ -48,8 +50,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
`;
log("###", sql);
return sql;
}
if (group_by) {
@@ -64,8 +64,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
WHERE rank <= ${per_page}
`;
log("###", sql);
return sql;
}
@@ -78,8 +76,10 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
sql += `SELECT fi.* , `;
if (order_by.includes("assignee")) {
sql += ` s.first_name as ${name} `;
} else if (order_by.includes("estimate")) {
sql += ` s.key as ${name} `;
} else {
sql += ` s.name as ${name} `;
sql += ` s.name as ${name} `;
}
sql += `FROM fi `;
if (order_by && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
@@ -87,7 +87,7 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
sql += `
LEFT JOIN cycles s on fi.cycle_id = s.id`;
}
if (order_by.includes("estimate_point")) {
if (order_by.includes("estimate_point__key")) {
sql += `
LEFT JOIN estimate_points s on fi.estimate_point = s.id`;
}
@@ -120,7 +120,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
`;
sql += ` group by i.id ${orderByString} LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
log("######$$$", sql);
return sql;
}
@@ -149,7 +148,6 @@ export const issueFilterQueryConstructor = (workspaceSlug: string, projectId: st
// Add offset and paging to query
sql += ` LIMIT ${pageSize} OFFSET ${offset * 1 + page * pageSize};`;
log("$$$", sql);
return sql;
};
+4 -1
View File
@@ -45,7 +45,7 @@ export const translateQueryParams = (queries: any) => {
}
// Fix invalid orderby when switching from spreadsheet layout
if (layout === "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
if (layout !== "spreadsheet" && Object.keys(SPECIAL_ORDER_BY).includes(order_by)) {
otherProps.order_by = "sort_order";
}
// For each property value, replace None with empty string
@@ -336,6 +336,9 @@ const getSingleFilterFields = (queries: any) => {
if (state_group) {
fields.add("states.'group' as state_group");
}
if (order_by?.includes("estimate_point__key")) {
fields.add("estimate_point");
}
return Array.from(fields);
};
@@ -95,7 +95,7 @@ export class ProjectPageService extends APIService {
});
}
async archive(
async moveToTrash(
workspaceSlug: string,
projectId: string,
pageId: string
@@ -109,7 +109,7 @@ export class ProjectPageService extends APIService {
});
}
async restore(workspaceSlug: string, projectId: string, pageId: string): Promise<void> {
async restoreFromTrash(workspaceSlug: string, projectId: string, pageId: string): Promise<void> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/archive/`)
.then((response) => response?.data)
.catch((error) => {
+1 -1
View File
@@ -388,7 +388,7 @@ export class ProjectInboxStore implements IProjectInboxStore {
else this.loader = "mutation-loading";
if (loadingType) this.loader = loadingType;
const status = this.inboxFilters?.status && uniq([...this.inboxFilters.status, EInboxIssueStatus.SNOOZED]);
const status = this.inboxFilters?.status;
const queryParams = this.inboxIssueQueryParams(
{ ...this.inboxFilters, status },
this.inboxSorting,
@@ -36,6 +36,7 @@ import { EIssueLayoutTypes, ISSUE_PRIORITIES } from "@/constants/issue";
// helpers
import { convertToISODateString } from "@/helpers/date-time.helper";
// local-db
import { SPECIAL_ORDER_BY } from "@/local-db/utils/query-constructor";
import { updatePersistentLayer } from "@/local-db/utils/utils";
// services
import { CycleService } from "@/services/cycle.service";
@@ -164,8 +165,8 @@ const ISSUE_ORDERBY_KEY: Record<TIssueOrderByOptions, keyof TIssue> = {
"-issue_cycle__cycle__name": "cycle_id",
target_date: "target_date",
"-target_date": "target_date",
estimate_point: "estimate_point",
"-estimate_point": "estimate_point",
estimate_point__key: "estimate_point",
"-estimate_point__key": "estimate_point",
start_date: "start_date",
"-start_date": "start_date",
link_count: "link_count",
@@ -282,6 +283,19 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
const displayFilters = this.issueFilterStore?.issueFilters?.displayFilters;
if (!displayFilters) return;
const layout = displayFilters.layout;
const orderBy = displayFilters.order_by;
// Temporary code to fix no load order by
if (
this.rootIssueStore.rootStore.user.localDBEnabled &&
layout !== EIssueLayoutTypes.SPREADSHEET &&
orderBy &&
Object.keys(SPECIAL_ORDER_BY).includes(orderBy)
) {
return "sort_order";
}
return displayFilters?.order_by;
}
@@ -1701,13 +1715,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
* @returns string | string[] of sortable fields to be used for sorting
*/
populateIssueDataForSorting(
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id" | "estimate_point",
dataIds: string | string[] | null | undefined,
projectId: string | undefined | null,
order?: "asc" | "desc"
) {
if (!dataIds) return;
const dataValues: string[] = [];
const dataValues: (string | number)[] = [];
const isDataIdsArray = Array.isArray(dataIds);
const dataIdsArray = isDataIdsArray ? dataIds : [dataIds];
@@ -1757,6 +1772,26 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
}
break;
}
case "estimate_point": {
// return if project Id does not exist
if (!projectId) break;
// get the estimate ID for the current Project
const currentProjectEstimateId =
this.rootIssueStore.rootStore.projectEstimate.currentActiveEstimateIdByProjectId(projectId);
// return if current Estimate Id for the project is not available
if (!currentProjectEstimateId) break;
// get Estimate based on Id
const estimate = this.rootIssueStore.rootStore.projectEstimate.estimateById(currentProjectEstimateId);
// If Estimate is not available, then return
if (!estimate) break;
// Get Estimate Value
const estimateKey = estimate?.estimatePointById(dataIds as string)?.key;
// If Value string i not available or empty then return
if (estimateKey === undefined) break;
dataValues.push(estimateKey);
}
}
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues;
@@ -1771,11 +1806,17 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return getIssueIds(orderBy(array, "sort_order"));
case "state__name":
return getIssueIds(
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]))
orderBy(array, (issue) =>
this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"])
)
);
case "-state__name":
return getIssueIds(
orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"]), ["desc"])
orderBy(
array,
(issue) => this.populateIssueDataForSorting("state_id", issue?.["state_id"], issue?.["project_id"]),
["desc"]
)
);
// dates
case "created_at":
@@ -1826,15 +1867,23 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
case "-attachment_count":
return getIssueIds(orderBy(array, "attachment_count", ["desc"]));
case "estimate_point":
case "estimate_point__key":
return getIssueIds(
orderBy(array, [getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"])
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
(issue) =>
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
])
); //preferring sorting based on empty values to always keep the empty values below
case "-estimate_point":
case "-estimate_point__key":
return getIssueIds(
orderBy(
array,
[getSortOrderToFilterEmptyValues.bind(null, "estimate_point"), "estimate_point"], //preferring sorting based on empty values to always keep the empty values below
[
getSortOrderToFilterEmptyValues.bind(null, "estimate_point"),
(issue) =>
this.populateIssueDataForSorting("estimate_point", issue?.["estimate_point"], issue?.["project_id"]),
], //preferring sorting based on empty values to always keep the empty values below
["asc", "desc"]
)
);
@@ -1854,7 +1903,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return getIssueIds(
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
])
);
case "-labels__name":
@@ -1863,7 +1913,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
array,
[
getSortOrderToFilterEmptyValues.bind(null, "label_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("label_ids", issue?.["label_ids"], issue?.["project_id"], "asc"),
],
["asc", "desc"]
)
@@ -1873,7 +1924,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return getIssueIds(
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
])
);
case "-issue_module__module__name":
@@ -1882,7 +1934,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
array,
[
getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("module_ids", issue?.["module_ids"], issue?.["project_id"], "asc"),
],
["asc", "desc"]
)
@@ -1892,7 +1945,7 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return getIssueIds(
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
])
);
case "-issue_cycle__cycle__name":
@@ -1901,7 +1954,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
array,
[
getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], "asc"),
(issue) =>
this.populateIssueDataForSorting("cycle_id", issue?.["cycle_id"], issue?.["project_id"], "asc"),
],
["asc", "desc"]
)
@@ -1911,7 +1965,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
return getIssueIds(
orderBy(array, [
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
])
);
case "-assignees__first_name":
@@ -1920,7 +1975,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
array,
[
getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
(issue) => this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], "asc"),
(issue) =>
this.populateIssueDataForSorting("assignee_ids", issue?.["assignee_ids"], issue?.["project_id"], "asc"),
],
["asc", "desc"]
)
+31 -24
View File
@@ -22,7 +22,7 @@ export interface IPage extends TPage {
canCurrentUserDuplicatePage: boolean;
canCurrentUserLockPage: boolean;
canCurrentUserChangeAccess: boolean;
canCurrentUserArchivePage: boolean;
canCurrentUserTrashPage: boolean;
canCurrentUserDeletePage: boolean;
canCurrentUserFavoritePage: boolean;
isContentEditable: boolean;
@@ -38,8 +38,8 @@ export interface IPage extends TPage {
makePrivate: () => Promise<void>;
lock: () => Promise<void>;
unlock: () => Promise<void>;
archive: () => Promise<void>;
restore: () => Promise<void>;
moveToTrash: () => Promise<void>;
restoreFromTrash: () => Promise<void>;
updatePageLogo: (logo_props: TLogoProps) => Promise<void>;
addToFavorites: () => Promise<void>;
removePageFromFavorites: () => Promise<void>;
@@ -132,7 +132,7 @@ export class Page implements IPage {
canCurrentUserDuplicatePage: computed,
canCurrentUserLockPage: computed,
canCurrentUserChangeAccess: computed,
canCurrentUserArchivePage: computed,
canCurrentUserTrashPage: computed,
canCurrentUserDeletePage: computed,
canCurrentUserFavoritePage: computed,
isContentEditable: computed,
@@ -144,8 +144,8 @@ export class Page implements IPage {
makePrivate: action,
lock: action,
unlock: action,
archive: action,
restore: action,
moveToTrash: action,
restoreFromTrash: action,
updatePageLogo: action,
addToFavorites: action,
removePageFromFavorites: action,
@@ -204,9 +204,11 @@ export class Page implements IPage {
};
}
/**
* @description returns true if the current logged in user is the owner of the page
*/
get isCurrentUserOwner() {
const currentUserId = this.store.user.data?.id;
if (!currentUserId) return false;
return this.owned_by === currentUserId;
}
@@ -215,7 +217,6 @@ export class Page implements IPage {
*/
get canCurrentUserEditPage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
@@ -228,7 +229,6 @@ export class Page implements IPage {
*/
get canCurrentUserDuplicatePage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
@@ -240,22 +240,31 @@ export class Page implements IPage {
* @description returns true if the current logged in user can lock the page
*/
get canCurrentUserLockPage() {
return this.isCurrentUserOwner;
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
);
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can change the access of the page
*/
get canCurrentUserChangeAccess() {
return this.isCurrentUserOwner;
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
);
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
}
/**
* @description returns true if the current logged in user can archive the page
* @description returns true if the current logged in user can trash the page
*/
get canCurrentUserArchivePage() {
get canCurrentUserTrashPage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
@@ -268,7 +277,6 @@ export class Page implements IPage {
*/
get canCurrentUserDeletePage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
@@ -281,7 +289,6 @@ export class Page implements IPage {
*/
get canCurrentUserFavoritePage() {
const { workspaceSlug, projectId } = this.store.router;
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
workspaceSlug?.toString() || "",
projectId?.toString() || ""
@@ -301,11 +308,11 @@ export class Page implements IPage {
projectId?.toString() || ""
);
const isPublic = this.access === EPageAccess.PUBLIC;
const isArchived = this.archived_at;
const isTrashed = this.archived_at;
const isLocked = this.is_locked;
return (
!isArchived &&
!isTrashed &&
!isLocked &&
(isOwner || (isPublic && !!currentUserRole && currentUserRole >= EUserPermissions.MEMBER))
);
@@ -469,12 +476,12 @@ export class Page implements IPage {
};
/**
* @description archive the page
* @description move the page to trash
*/
archive = async () => {
moveToTrash = async () => {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
const response = await this.pageService.archive(workspaceSlug, projectId, this.id);
const response = await this.pageService.moveToTrash(workspaceSlug, projectId, this.id);
runInAction(() => {
this.archived_at = response.archived_at;
});
@@ -482,12 +489,12 @@ export class Page implements IPage {
};
/**
* @description restore the page
* @description restore the page from trash
*/
restore = async () => {
restoreFromTrash = async () => {
const { workspaceSlug, projectId } = this.store.router;
if (!workspaceSlug || !projectId || !this.id) return undefined;
await this.pageService.restore(workspaceSlug, projectId, this.id);
await this.pageService.restoreFromTrash(workspaceSlug, projectId, this.id);
runInAction(() => {
this.archived_at = null;
});
+15 -16
View File
@@ -31,12 +31,8 @@ export interface IProjectPageStore {
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
clearAllFilters: () => void;
// actions
getAllPages: (
workspaceSlug: string,
projectId: string,
pageType: TPageNavigationTabs
) => Promise<TPage[] | undefined>;
getPageById: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
fetchAllPages: (workspaceSlug: string, projectId: string) => Promise<TPage[]>;
fetchPageById: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage>;
createPage: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
removePage: (pageId: string) => Promise<void>;
}
@@ -66,8 +62,8 @@ export class ProjectPageStore implements IProjectPageStore {
updateFilters: action,
clearAllFilters: action,
// actions
getAllPages: action,
getPageById: action,
fetchAllPages: action,
fetchPageById: action,
createPage: action,
removePage: action,
});
@@ -154,13 +150,14 @@ export class ProjectPageStore implements IProjectPageStore {
/**
* @description fetch all the pages
*/
getAllPages = async (workspaceSlug: string, projectId: string, pageType: TPageNavigationTabs) => {
fetchAllPages = async (workspaceSlug: string, projectId: string) => {
try {
if (!workspaceSlug || !projectId) return undefined;
if (!workspaceSlug || !projectId) {
throw new Error("workspace slug or project id not provided");
}
const currentPageIds = this.getCurrentProjectPageIds(pageType);
runInAction(() => {
this.loader = currentPageIds && currentPageIds.length > 0 ? `mutation-loader` : `init-loader`;
this.loader = this.isAnyPageAvailable ? `mutation-loader` : `init-loader`;
this.error = undefined;
});
@@ -187,13 +184,15 @@ export class ProjectPageStore implements IProjectPageStore {
* @description fetch the details of a page
* @param {string} pageId
*/
getPageById = async (workspaceSlug: string, projectId: string, pageId: string) => {
fetchPageById = async (workspaceSlug: string, projectId: string, pageId: string) => {
try {
if (!workspaceSlug || !projectId || !pageId) return undefined;
if (!workspaceSlug || !projectId || !pageId) {
throw new Error("workspace slug, project id or page id not provided");
}
const currentPageId = this.pageById(pageId);
const currentPage = this.pageById(pageId);
runInAction(() => {
this.loader = currentPageId ? `mutation-loader` : `init-loader`;
this.loader = currentPage ? `mutation-loader` : `init-loader`;
this.error = undefined;
});
+1
View File
@@ -0,0 +1 @@
export * from "ce/components/instance";
@@ -0,0 +1 @@
export const MaintenanceMessage = () => <></>;
-1
View File
@@ -1 +0,0 @@
export * from "ce/components/maintenance-mode";
+1 -1
View File
@@ -14,7 +14,7 @@ export const filterPagesByPageType = (pageType: TPageNavigationTabs, pages: TPag
pages.filter((page) => {
if (pageType === "public") return page.access === 0 && !page.archived_at;
if (pageType === "private") return page.access === 1 && !page.archived_at;
if (pageType === "archived") return page.archived_at;
if (pageType === "trash") return page.archived_at;
return true;
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

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