refactor: MobX store structure (#3228)

* query params from router as computed

* chore: setup workspace store and sub-stores

* chore: update router query store

* chore: update store types

* fix: pages store changes

* change observables and retain object reference

* fix build errors

* chore: changed the structure of workspace, project, cycle, module and pages

* fix: pages fixes

* fix: merge conflicts resolved

* chore: fixed workspace list

* chore: update workspace store accroding to the new response

* fix: adding page details to store

* fix: adding new contexts and providers

* dev: issues store and filters in new store

* dev: optimised the issue fetching in issue base store

* chore: project views id mapped

* update lodash set to directly run inside runInaction since it mutates the object

* fix: context changes

* code refactor kanban for better mainatinability

* optimize Kanban for performance

* chore: implemented hooks for all the created stores

* chore: removed bridge id

* css change and refactor

* chore: update cycle store structure

* chore: implement the new label root store

* chore: removed object structure

* chore: implement project view hook

* Kanban new store implementation for project issues

* fix project root for kanban

* feat: workspace and project members endpoint (#3092)

* fix: merge conflicts resolved

* issue properties optimization

* chore: user stores

* chore: create new store context and update hooks

* chore: setup inbox store and implement router store

* chore: initialize and implement project estimate store

* chore: initialize global view store

* kanban and list view optimization

* chore: use new cycle and module store. (#3172)

* chore: use new cycle and module store.

* chore: minor improvements.

* Revert "chore: merge develop"

This reverts commit 9d2e0e29e7370b55b48fc2fee4fd126093a6cc48, reversing
changes made to 9595493c42be3ea0ddd17b23a0b124555075c062.

* chore: implement useGlobalView hook

* refactor: projects & inbox store instances (#3179)

* refactor: projects & inbox store instances

* fix: formatting

* fix: action usage

* chore: implement useProjectState hook. (#3185)

* dev: issue, cycle store optimiation

* fix build for code

* dev: removed dummy variables

* dev: issue store

* fix: adding todos

* chore: removing legacy store

* dev: issues store types and typos

* chore: cycle module user properties

* fix legacy store deletion issues

* chore: change POST to PATCH

* fix issues rendering for project root

* chore: removed workspace details in workpsaceinvite

* chore: created models for display properties

* chore: setup member store and implement it everywhere

* refactor: module store (#3202)

* refactor: cycle store (#3192)

* refator: cycle store

* some more improvements.

* chore: implement useLabel hook. (#3190)

* refactor: inbox & project related stores. (#3193)

* refactor: inbox -> filter, issues, inoxes & project -> publish, projects store

* refactor: workspace-project-id name

* fix kanban dropdown overlapping issue

* fix kanban layout minor re rendering

* chore: implement useMember store everywhere

* chore: create and implement editor mention store

* chore: removed the issue view user property

* chore: created at id changed

* dev: segway intgegration (#3132)

* feat: implemented rabbitmq

* dev: initialize segway with queue setup

* dev: import refactors

* dev: create communication with the segway server

* dev: create new workers

* dev: create celery node queue for consuming messages from django

* dev: node to celery connection

* dev: setup segway and django connection

* dev: refactor the structure and add database integration to the app

* dev: add external id and source added

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>

* dev: github importer (#3205)

* dev: initiate github import

* dev: github importer all issues import

* dev: github comments and links for the imported issues

* dev: update controller to use logger and spread the resultData in getAllEntities

* dev: removed console log

* dev: update code structure and sync functions

* dev: updated retry logic when exception

* dev: add imported data as well

* dev: update logger and repo fetch

* dev: update jira integration to new structure

* dev: update migrations

* dev: update the reason field

* chore: workspace object id removed

* chore: view's creation fixed

* refactor: mobx store improvements. (#3213)

* fix: state and label errors

* chore: remove legacy code

* fix: branch build fix (#3214)

* branch build fix for release-* in case of space,backend,proxy

* fixes

* chore: update store names and types

* fix - file size limit not work on plane.settings.production (#3160)

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production

* fix - file size limit not work on plane.settings.production, move to common.py

---------

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

* style: instance admin email settings ui & ux update. (#3186)

* refactor: use-user-auth hook (#3215)

* refactor: use-user-auth hook

* fix: user store currentUserLoader

* refactor: project-view & application related stores (#3207)

* refactor: project-view & application related stores

* rename: projectViews -> projectViewIds

* fix: project-view favourite state in store

* chore: remove unnecessary hooks and contexts (#3217)

* chore: update issue assignee property component

* chore: bug fixes & improvement (#3218)

* chore: draft issue validation added to prevent saving empty or whitespace title

* chore: resolve scrolling issue in page empty state

* chore: kanban layout quick add issue improvement

* fix: bugs & improvements (#3189)

* fix: workspace invitation modal form values reset

* fix: profile sidebar avatar letter

* [refactor] Editor code refactoring (#3194)

* removed relative imports from editor core

* Update issue widget file paths and imports to use kebab case instead of camel case, to align with coding conventions and improve consistency.

* Update Tiptap core and extensions versions to 2.1.13 and Tiptap React version to 2.1.13. Update Tiptap table imports to use the new location in package @tiptap/pm/tables. Update AlertLabel component to use the new type definition for LucideIcon.

* updated lock file

* removed default exports from editor/core

* fixed injecting css into the core package itself

* seperated css code to have single source of origin wrt to the package

* removed default imports from document editor

* all instances using index as key while mapping fixed

* Update Lite Text Editor package.json to remove @plane/editor-types as a dependency.

Update Lite Text Editor index.ts to update the import of IMentionSuggestion and IMentionHighlight from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/index.tsx to update the import of UploadImage, DeleteImage, IMentionSuggestion, and RestoreImage from @plane/editor-types to @plane/editor-core.

Update Lite Text Editor ui/menus/fixed-menu/index.tsx to update the import of UploadImage from @plane/editor-types to @plane/editor-core.

Update turbo.json to remove @plane/editor-types#build as a dependency for @plane/lite-text-editor#build, @plane/rich-text-editor#build, and @plane/document-editor#build.

* Remove deprecated import and adjust tippy.js usage in the slash-commands.tsx file of the editor extensions package.

* Update dependencies in `rich-text-editor/package.json`, remove `@plane/editor-types` and add `@plane/editor-core` in `rich-text-editor/src/index.ts`, and update imports in `rich-text-editor/src/ui/extensions/index.tsx` and `rich-text-editor/src/ui/index.tsx` to use `@plane/editor-core` instead of `@plane/editor-types`.

* Update package.json dependencies and add new types for image deletion, upload, restore, mention highlight, mention suggestion, and slash command item.

* Update import statements in various files to use the new package "@plane/editor-core" instead of "@plane/editor-types".

* fixed document editor to follow conventions

* Refactor imports in the Rich Text Editor package to use relative paths instead of absolute paths.

- Updated imports in `index.ts`, `ui/index.tsx`, and `ui/menus/bubble-menu/index.tsx` to use relative paths.
- Updated `tsconfig.json` to include the `baseUrl` compiler option and adjust the `include` and `exclude` paths.

* Refactor Lite Text Editor code to use relative import paths instead of absolute import paths.

* Added LucideIconType to the exports in index.ts for use in other files.
Created a new file lucide-icon.ts which contains the type LucideIconType.
Updated the icon type in HeadingOneItem in menu-items/index.tsx to use LucideIconType.
Updated the Icon type in AlertLabel in alert-label.tsx to use LucideIconType.
Updated the Icon type in VerticalDropdownItemProps in vertical-dropdown-menu.tsx to use LucideIconType.
Updated the Icon type in BubbleMenuItem in fixed-menu/index.tsx to use LucideIconType.
Deleted the file tooltip.tsx since it is no longer used.
Updated the Icon type in BubbleMenuItem in bubble-menu/index.tsx to use LucideIconType.

* ♻️ refactor: simplify rendering logic in slash-commands.tsx

The rendering logic in the file "slash-commands.tsx" has been simplified. Previously, the code used inline positioning for the popup, but it has now been removed. Instead of appending the popup to the document body, it is now appended to the element with the ID "tiptap-container". The "flip" option has also been removed. These changes have improved the readability and maintainability of the code.

* fixed build errors caused due to core's internal imports

* regression: fixed pages not saving issue and not duplicating with proper content issue

* build: Update @tiptap dependencies

Updated the @tiptap dependencies in the package.json files of `document-editor`, `extensions`, and `rich-text-editor` packages to version 2.1.13.

* 🚑 fix: Correct appendTo selector in slash-commands.tsx

Update the `appendTo` function call in `slash-commands.tsx` to use the correct selector `#editor-container` instead of `#tiptap-container`. This ensures that the component is appended to the appropriate container in the editor extension.

Note: The commit message assumes that the change is a fix for an issue or error. If it's not a fix, please provide more context so that an appropriate commit type can be determined.

* style: email placeholder changed across the platform (#3206)

* style: email placeholder changed across the platform

* fix: placeholder text

* dev: updated new filter endpoints and restructured issue and issue filters store

* implement issues and replace useMobxStore

* remove all store legacy references

* dev: updated the orderby and subgroupby filters data

* dev:added projectId in issue filters for consistency

* fix more build errors

* dev: updated profile issues

* dev: removed store legacy

* dev: active cycle issues in the cycle issue store

* fix additional build errors and memoize issueActions in each layout component

* change store enums

* remove all useMobxStore references

* fix more build errors

* dev: reverted workspace invitation

* fix: build errors and warnings

* fix: optimistic update for instant operations (#3221)

* fix: update functions failed case

* fix: typo

* chore: revert back to optimistic update approach for all `update related actions` (#3219)

* fix: merge conflicts resolved

* chore: update memberMap logic in components

* add assignees to kanban groups and properties

* dev: migration fixes

* final bit of optimization on list view

* change all TODOs that are to be done before this release to FIXME

* change base Kanban TODOs that are to be done before this release to FIXME

* dev: add fields and expand for app serializers

* dev: issue detail store

* dev: update issue serializer to return object ids

* fix: Instance key added in settings and converted issues list api to arry instead of dict

* fix: removing segway files

* dev: control expand through query parameters

* revert: github importer

* Revert "dev: segway intgegration (#3132)"

This reverts commit 1cc18a09156d1790d114061dbac8c901e0f2754c.

* dev: remove migrations for segway

* dev: issue structure change and created workspacebasemodel

* dev: issue detail serializer

* fix: changed workspace dict

* dev: updated new issue structure

* chore: build fix

* dev: issue detail store refactor

* dev: created list endpoint for issue-relation

* dev: added issue attachments in issue detail store

* dev: added issue activity computed

* fix: build error

* chore: peek overview modal context added

* chore: build error fix

* dev: added sub_issues in issue details store

* dev: added complete issue serializer for sub issues

* dev: resolved type errors in issue root store

* dev: changed the issue relation structure

* chore: new global dropdowns

* chore: build error fix

* chore: cycle and module selection if disabled

* dev: removed unnecessary code from the workspace root

* chore: build error fix

* chore: issue relation remove endpoint

* fix: build error

* dev: typos and implemented issue relation store

* fix: yarn lock updated

* style: update the UI of all the dropdowns

* fix: state store fixes

* fix: key issue

* fix: state store console logs removed

* refactor: member dropdowns

* fix: moving types to packages

* fix: dropdown arrow positioning

* dev: removed logs

* style: label dropdown

* chore: restrict description notifications

* chore: description changes

* chore: update spreadsheet layout dropdowns

* fix: build errors

* chore: duplicate key change

* fix: ui bugs

* chore: relation activity change

* chore: comment activity changes

* chore: blocking issue removal

* chore: added project_id for relation

* chore: issue relation store and component

* chore: issue redirection issue in the issue realtion in detail page

* chore: created activity changed

* chore: issue links new store implementation on the issue detail

* chore: issue relation deletion acitivity changed

* chore: issue attachments new store implementation on the issue detail

* chore: workspace level issues

* fix: build errors

---------

Co-authored-by: rahulramesha <rahulramesham@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Hoang Luan <luandnh98@gmail.com>
Co-authored-by: luanduongtel4vn <hoangluan@tel4vn.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: M. Palanikannan <73993394+Palanikannan1437@users.noreply.github.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
Aaryan Khandelwal 2024-01-02 18:12:55 +05:30 committed by GitHub
parent 1539340113
commit 804b7d8663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
940 changed files with 26378 additions and 34411 deletions

View File

@ -3,7 +3,7 @@ name: Create Sync Action
on: on:
pull_request: pull_request:
branches: branches:
- preview - develop # Change this to preview
types: types:
- closed - closed
env: env:
@ -33,14 +33,23 @@ jobs:
sudo apt update sudo apt update
sudo apt install gh -y sudo apt install gh -y
- name: Push Changes to Target Repo - name: Create Pull Request
env: env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: | run: |
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}" TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH git checkout $SOURCE_BRANCH
git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git" git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH
PR_TITLE=${{secrets.SYNC_PR_TITLE}}
gh pr create \
--base $TARGET_BASE_BRANCH \
--head $TARGET_BRANCH \
--title "$PR_TITLE" \
--repo $TARGET_REPO

View File

@ -97,7 +97,7 @@ class BaseSerializer(serializers.ModelSerializer):
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](
getattr(instance, expand) getattr(instance, expand)
) )
response[expand] = exp_serializer.data response[expand] = exp_serializer.data
else: else:
# You might need to handle this case differently # You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None) response[expand] = getattr(instance, f"{expand}_id", None)

View File

@ -17,6 +17,7 @@ from .workspace import (
WorkspaceThemeSerializer, WorkspaceThemeSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer, WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
) )
from .project import ( from .project import (
ProjectSerializer, ProjectSerializer,
@ -31,6 +32,7 @@ from .project import (
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer, ProjectPublicMemberSerializer,
ProjectMemberRoleSerializer,
) )
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
@ -39,6 +41,7 @@ from .cycle import (
CycleIssueSerializer, CycleIssueSerializer,
CycleFavoriteSerializer, CycleFavoriteSerializer,
CycleWriteSerializer, CycleWriteSerializer,
CycleUserPropertiesSerializer,
) )
from .asset import FileAssetSerializer from .asset import FileAssetSerializer
from .issue import ( from .issue import (
@ -61,6 +64,8 @@ from .issue import (
IssueRelationSerializer, IssueRelationSerializer,
RelatedIssueSerializer, RelatedIssueSerializer,
IssuePublicSerializer, IssuePublicSerializer,
IssueRelationLiteSerializer,
) )
from .module import ( from .module import (
@ -69,6 +74,7 @@ from .module import (
ModuleIssueSerializer, ModuleIssueSerializer,
ModuleLinkSerializer, ModuleLinkSerializer,
ModuleFavoriteSerializer, ModuleFavoriteSerializer,
ModuleUserPropertiesSerializer,
) )
from .api import APITokenSerializer, APITokenReadSerializer from .api import APITokenSerializer, APITokenReadSerializer

View File

@ -9,11 +9,12 @@ class DynamicBaseSerializer(BaseSerializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# If 'fields' is provided in the arguments, remove it and store it separately. # If 'fields' is provided in the arguments, remove it and store it separately.
# This is done so as not to pass this custom argument up to the superclass. # This is done so as not to pass this custom argument up to the superclass.
fields = kwargs.pop("fields", None) fields = kwargs.pop("fields", [])
self.expand = kwargs.pop("expand", []) or []
fields = self.expand
# Call the initialization of the superclass. # Call the initialization of the superclass.
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# If 'fields' was provided, filter the fields of the serializer accordingly. # If 'fields' was provided, filter the fields of the serializer accordingly.
if fields is not None: if fields is not None:
self.fields = self._filter_fields(fields) self.fields = self._filter_fields(fields)
@ -47,12 +48,91 @@ class DynamicBaseSerializer(BaseSerializer):
elif isinstance(item, dict): elif isinstance(item, dict):
allowed.append(list(item.keys())[0]) allowed.append(list(item.keys())[0])
# Convert the current serializer's fields and the allowed fields to sets. for field in allowed:
existing = set(self.fields) if field not in self.fields:
allowed = set(allowed) from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueFlatSerializer,
)
# Remove fields from the serializer that aren't in the 'allowed' list. # Expansion mapper
for field_name in (existing - allowed): expansion = {
self.fields.pop(field_name) "user": UserLiteSerializer,
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueFlatSerializer,
}
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
return self.fields return self.fields
def to_representation(self, instance):
response = super().to_representation(instance)
# Ensure 'expand' is iterable before processing
if self.expand:
for expand in self.expand:
if expand in self.fields:
# Import all the expandable serializers
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
)
# Expansion mapper
expansion = {
"user": UserLiteSerializer,
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
}
# Check if field in expansion then expand the field
if expand in expansion:
if isinstance(response.get(expand), list):
exp_serializer = expansion[expand](
getattr(instance, expand), many=True
)
else:
exp_serializer = expansion[expand](
getattr(instance, expand)
)
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
return response

View File

@ -7,7 +7,7 @@ from .user import UserLiteSerializer
from .issue import IssueStateSerializer from .issue import IssueStateSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
class CycleWriteSerializer(BaseSerializer): class CycleWriteSerializer(BaseSerializer):
@ -106,3 +106,15 @@ class CycleFavoriteSerializer(BaseSerializer):
"project", "project",
"user", "user",
] ]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"cycle"
"user",
]

View File

@ -49,7 +49,6 @@ class IssueStateInboxSerializer(BaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
class Meta: class Meta:

View File

@ -278,17 +278,28 @@ class IssueLabelSerializer(BaseSerializer):
] ]
class IssueRelationLiteSerializer(DynamicBaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Issue
fields = [
"id",
"project_id",
"sequence_id",
]
read_only_fields = [
"workspace",
"project",
]
class IssueRelationSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue")
class Meta: class Meta:
model = IssueRelation model = IssueRelation
fields = [ fields = [
"issue_detail", "issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
] ]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
@ -296,16 +307,12 @@ class IssueRelationSerializer(BaseSerializer):
] ]
class RelatedIssueSerializer(BaseSerializer): class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue")
class Meta: class Meta:
model = IssueRelation model = IssueRelation
fields = [ fields = [
"issue_detail", "issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
] ]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
@ -512,7 +519,6 @@ class IssueStateSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True)
@ -521,32 +527,58 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__" fields = "__all__"
class IssueSerializer(BaseSerializer): class IssueSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") # ids
state_detail = StateSerializer(read_only=True, source="state") project_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") state_id = serializers.PrimaryKeyRelatedField(read_only=True)
label_details = LabelSerializer(read_only=True, source="labels", many=True) parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) module_id = serializers.PrimaryKeyRelatedField(read_only=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True) # Many to many
issue_module = IssueModuleDetailSerializer(read_only=True) label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels")
issue_link = IssueLinkSerializer(read_only=True, many=True) assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees")
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True) sub_issues_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True) attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
# is
is_subscribed = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Issue model = Issue
fields = "__all__" fields = [
read_only_fields = [ "id",
"workspace", "name",
"project", "state_id",
"created_by", "description_html",
"updated_by", "sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_id",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at", "created_at",
"updated_at", "updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_subscribed",
"is_draft",
"archived_at",
] ]
read_only_fields = fields
class IssueLiteSerializer(DynamicBaseSerializer): class IssueLiteSerializer(DynamicBaseSerializer):

View File

@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer from .user import UserLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
@ -14,6 +14,7 @@ from plane.db.models import (
ModuleIssue, ModuleIssue,
ModuleLink, ModuleLink,
ModuleFavorite, ModuleFavorite,
ModuleUserProperties,
) )
@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer):
return ModuleLink.objects.create(**validated_data) return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(BaseSerializer): class ModuleSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead") lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members") members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
@ -196,3 +197,14 @@ class ModuleFavoriteSerializer(BaseSerializer):
"project", "project",
"user", "user",
] ]
class ModuleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = ModuleUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"module",
"user"
]

View File

@ -159,6 +159,11 @@ class ProjectMemberAdminSerializer(BaseSerializer):
model = ProjectMember model = ProjectMember
fields = "__all__" fields = "__all__"
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
class ProjectMemberInviteSerializer(BaseSerializer): class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True) project = ProjectLiteSerializer(read_only=True)

View File

@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from .workspace import WorkspaceLiteSerializer from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite from plane.db.models import GlobalView, IssueView, IssueViewFavorite
@ -38,7 +38,7 @@ class GlobalViewSerializer(BaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class IssueViewSerializer(BaseSerializer): class IssueViewSerializer(DynamicBaseSerializer):
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True) project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
@ -80,4 +80,4 @@ class IssueViewFavoriteSerializer(BaseSerializer):
"workspace", "workspace",
"project", "project",
"user", "user",
] ]

View File

@ -2,7 +2,7 @@
from rest_framework import serializers from rest_framework import serializers
# Module imports # Module imports
from .base import BaseSerializer from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import ( from plane.db.models import (
@ -13,10 +13,11 @@ from plane.db.models import (
TeamMember, TeamMember,
WorkspaceMemberInvite, WorkspaceMemberInvite,
WorkspaceTheme, WorkspaceTheme,
WorkspaceUserProperties,
) )
class WorkSpaceSerializer(BaseSerializer): class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True) owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
@ -62,7 +63,7 @@ class WorkspaceLiteSerializer(BaseSerializer):
class WorkSpaceMemberSerializer(BaseSerializer): class WorkSpaceMemberSerializer(DynamicBaseSerializer):
member = UserLiteSerializer(read_only=True) member = UserLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
fields = "__all__" fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer): class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
member = UserAdminLiteSerializer(read_only=True) member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True) workspace = WorkspaceLiteSerializer(read_only=True)
@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
"workspace", "workspace",
"actor", "actor",
] ]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = [
"workspace",
"user",
]

View File

@ -7,6 +7,7 @@ from plane.app.views import (
CycleDateCheckEndpoint, CycleDateCheckEndpoint,
CycleFavoriteViewSet, CycleFavoriteViewSet,
TransferCycleIssueEndpoint, TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
) )
@ -44,7 +45,7 @@ urlpatterns = [
name="project-issue-cycle", name="project-issue-cycle",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
CycleIssueViewSet.as_view( CycleIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
@ -84,4 +85,9 @@ urlpatterns = [
TransferCycleIssueEndpoint.as_view(), TransferCycleIssueEndpoint.as_view(),
name="transfer-issues", name="transfer-issues",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/user-properties/",
CycleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
)
] ]

View File

@ -40,7 +40,7 @@ urlpatterns = [
name="inbox-issue", name="inbox-issue",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueViewSet.as_view( InboxIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",

View File

@ -235,7 +235,7 @@ urlpatterns = [
## End Comment Reactions ## End Comment Reactions
## IssueProperty ## IssueProperty
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-display-properties/", "workspaces/<str:slug>/projects/<uuid:project_id>/user-properties/",
IssueUserDisplayPropertyEndpoint.as_view(), IssueUserDisplayPropertyEndpoint.as_view(),
name="project-issue-display-properties", name="project-issue-display-properties",
), ),
@ -275,16 +275,17 @@ urlpatterns = [
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view( IssueRelationViewSet.as_view(
{ {
"get": "list",
"post": "create", "post": "create",
} }
), ),
name="issue-relation", name="issue-relation",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view( IssueRelationViewSet.as_view(
{ {
"delete": "destroy", "post": "remove_relation",
} }
), ),
name="issue-relation", name="issue-relation",

View File

@ -7,6 +7,7 @@ from plane.app.views import (
ModuleLinkViewSet, ModuleLinkViewSet,
ModuleFavoriteViewSet, ModuleFavoriteViewSet,
BulkImportModulesEndpoint, BulkImportModulesEndpoint,
ModuleUserPropertiesEndpoint
) )
@ -44,7 +45,7 @@ urlpatterns = [
name="project-module-issues", name="project-module-issues",
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
ModuleIssueViewSet.as_view( ModuleIssueViewSet.as_view(
{ {
"get": "retrieve", "get": "retrieve",
@ -101,4 +102,9 @@ urlpatterns = [
BulkImportModulesEndpoint.as_view(), BulkImportModulesEndpoint.as_view(),
name="bulk-modules-create", name="bulk-modules-create",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/user-properties/",
ModuleUserPropertiesEndpoint.as_view(),
name="cycle-user-filters",
)
] ]

View File

@ -5,7 +5,7 @@ from plane.app.views import (
IssueViewViewSet, IssueViewViewSet,
GlobalViewViewSet, GlobalViewViewSet,
GlobalViewIssuesViewSet, GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet, IssueViewFavoriteViewSet,
) )

View File

@ -18,6 +18,8 @@ from plane.app.views import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
) )
@ -92,6 +94,11 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"get": "list"}), WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member", name="workspace-member",
), ),
path(
"workspaces/<str:slug>/project-members/",
WorkspaceProjectMemberEndpoint.as_view(),
name="workspace-member-roles",
),
path( path(
"workspaces/<str:slug>/members/<uuid:pk>/", "workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view( WorkSpaceMemberViewSet.as_view(
@ -195,4 +202,9 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(), WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels", name="workspace-labels",
), ),
path(
"workspaces/<str:slug>/user-properties/",
WorkspaceUserPropertiesEndpoint.as_view(),
name="workspace-user-filters",
)
] ]

View File

@ -45,6 +45,8 @@ from .workspace import (
WorkspaceUserProfileEndpoint, WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint, WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
) )
from .state import StateViewSet from .state import StateViewSet
from .view import ( from .view import (
@ -59,6 +61,7 @@ from .cycle import (
CycleDateCheckEndpoint, CycleDateCheckEndpoint,
CycleFavoriteViewSet, CycleFavoriteViewSet,
TransferCycleIssueEndpoint, TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
) )
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue import ( from .issue import (
@ -103,6 +106,7 @@ from .module import (
ModuleIssueViewSet, ModuleIssueViewSet,
ModuleLinkViewSet, ModuleLinkViewSet,
ModuleFavoriteViewSet, ModuleFavoriteViewSet,
ModuleUserPropertiesEndpoint,
) )
from .api import ApiTokenEndpoint from .api import ApiTokenEndpoint

View File

@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
if resolve(self.request.path_info).url_name == "project": if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None) return self.kwargs.get("pk", None)
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None
class BaseAPIView(TimezoneMixin, APIView, BasePaginator): class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [ permission_classes = [
@ -239,3 +254,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property @property
def project_id(self): def project_id(self):
return self.kwargs.get("project_id", None) return self.kwargs.get("project_id", None)
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None

View File

@ -14,7 +14,7 @@ from django.db.models import (
Case, Case,
When, When,
Value, Value,
CharField CharField,
) )
from django.core import serializers from django.core import serializers
from django.utils import timezone from django.utils import timezone
@ -33,8 +33,9 @@ from plane.app.serializers import (
CycleFavoriteSerializer, CycleFavoriteSerializer,
IssueStateSerializer, IssueStateSerializer,
CycleWriteSerializer, CycleWriteSerializer,
CycleUserPropertiesSerializer,
) )
from plane.app.permissions import ProjectEntityPermission from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import ( from plane.db.models import (
User, User,
Cycle, Cycle,
@ -44,6 +45,7 @@ from plane.db.models import (
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
Label, Label,
CycleUserProperties,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -164,23 +166,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.annotate( .annotate(
status=Case( status=Case(
When( When(
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()), Q(start_date__lte=timezone.now())
then=Value("CURRENT") & Q(end_date__gte=timezone.now()),
), then=Value("CURRENT"),
When(
start_date__gt=timezone.now(),
then=Value("UPCOMING")
),
When(
end_date__lt=timezone.now(),
then=Value("COMPLETED")
), ),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When( When(
Q(start_date__isnull=True) & Q(end_date__isnull=True), Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT") then=Value("DRAFT"),
), ),
default=Value("DRAFT"), default=Value("DRAFT"),
output_field=CharField(), output_field=CharField(),
) )
) )
.prefetch_related( .prefetch_related(
@ -202,6 +199,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
queryset = self.get_queryset() queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", "all") cycle_view = request.GET.get("cycle_view", "all")
fields = [field for field in request.GET.get("fields", "").split(",") if field]
queryset = queryset.order_by("-is_favorite", "-created_at") queryset = queryset.order_by("-is_favorite", "-created_at")
@ -307,44 +305,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
# Upcoming Cycles cycles = CycleSerializer(queryset, many=True).data
if cycle_view == "upcoming": return Response(cycles, status=status.HTTP_200_OK)
queryset = queryset.filter(start_date__gt=timezone.now())
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Completed Cycles
if cycle_view == "completed":
queryset = queryset.filter(end_date__lt=timezone.now())
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Draft Cycles
if cycle_view == "draft":
queryset = queryset.filter(
end_date=None,
start_date=None,
)
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now().date()) | Q(end_date__isnull=True),
)
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
# If no matching view is found return all cycles
return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
if ( if (
@ -576,7 +538,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id) .filter(project_id=project_id)
.filter(workspace__slug=slug) .filter(workspace__slug=slug)
.select_related("project") .select_related("project")
@ -600,12 +561,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count") .values("count")
) )
) )
serializer = IssueStateSerializer(
issues = IssueStateSerializer(
issues, many=True, fields=fields if fields else None issues, many=True, fields=fields if fields else None
).data )
issue_dict = {str(issue["id"]): issue for issue in issues} return Response(serializer.data, status=status.HTTP_200_OK)
return Response(issue_dict, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, cycle_id): def create(self, request, slug, project_id, cycle_id):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
@ -698,11 +657,13 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
def destroy(self, request, slug, project_id, cycle_id, pk): def destroy(self, request, slug, project_id, cycle_id, issue_id):
cycle_issue = CycleIssue.objects.get( cycle_issue = CycleIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
cycle_id=cycle_id,
) )
issue_id = cycle_issue.issue_id
issue_activity.delay( issue_activity.delay(
type="cycle.activity.deleted", type="cycle.activity.deleted",
requested_data=json.dumps( requested_data=json.dumps(
@ -712,7 +673,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
} }
), ),
actor_id=str(self.request.user.id), actor_id=str(self.request.user.id),
issue_id=str(cycle_issue.issue_id), issue_id=str(issue_id),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
@ -834,3 +795,39 @@ class TransferCycleIssueEndpoint(BaseAPIView):
) )
return Response({"message": "Success"}, status=status.HTTP_200_OK) return Response({"message": "Success"}, status=status.HTTP_200_OK)
class CycleUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def patch(self, request, slug, project_id, cycle_id):
cycle_properties = CycleUserProperties.objects.get(
user=request.user,
cycle_id=cycle_id,
project_id=project_id,
workspace__slug=slug,
)
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
cycle_properties.display_properties = request.data.get(
"display_properties", cycle_properties.display_properties
)
cycle_properties.save()
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id, cycle_id):
cycle_properties, _ = CycleUserProperties.objects.get_or_create(
user=request.user,
project_id=project_id,
cycle_id=cycle_id,
workspace__slug=slug,
)
serializer = CycleUserPropertiesSerializer(cycle_properties)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -107,7 +107,6 @@ class InboxIssueViewSet(BaseViewSet):
project_id=project_id, project_id=project_id,
) )
.filter(**filters) .filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels") .prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status") .order_by("issue_inbox__snoozed_till", "issue_inbox__status")
@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, pk): def partial_update(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
) )
# Get the project member # Get the project member
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet):
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
) )
def retrieve(self, request, slug, project_id, inbox_id, pk): def retrieve(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
issue = Issue.objects.get( issue = Issue.objects.get(
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id pk=issue_id, workspace__slug=slug, project_id=project_id
) )
serializer = IssueStateInboxSerializer(issue) serializer = IssueStateInboxSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, pk): def destroy(self, request, slug, project_id, inbox_id, issue_id):
inbox_issue = InboxIssue.objects.get( inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id issue_id=issue_id, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
) )
# Get the project member # Get the project member
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet):
if inbox_issue.status in [-2, -1, 0, 2]: if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also # Delete the issue also
Issue.objects.filter( Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id workspace__slug=slug, project_id=project_id, pk=issue_id
).delete() ).delete()
inbox_issue.delete() inbox_issue.delete()

View File

@ -52,6 +52,7 @@ from plane.app.serializers import (
IssueRelationSerializer, IssueRelationSerializer,
RelatedIssueSerializer, RelatedIssueSerializer,
IssuePublicSerializer, IssuePublicSerializer,
IssueRelationLiteSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
ProjectEntityPermission, ProjectEntityPermission,
@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
queryset=IssueReaction.objects.select_related("actor"), queryset=IssueReaction.objects.select_related("actor"),
) )
) )
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id")) .annotate(module_id=F("issue_module__module_id"))
.annotate( .annotate(
@ -159,7 +144,26 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
) .annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
).distinct()
@method_decorator(gzip_page)
def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters)
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
@ -217,9 +221,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data issues = IssueSerializer(
issue_dict = {str(issue["id"]): issue for issue in issues} issue_queryset, many=True, fields=self.fields, expand=self.expand
return Response(issue_dict, status=status.HTTP_200_OK) ).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
@ -256,7 +261,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk) ).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
status=status.HTTP_200_OK,
)
def partial_update(self, request, slug, project_id, pk=None): def partial_update(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
ProjectLitePermission, ProjectLitePermission,
] ]
def post(self, request, slug, project_id): def patch(self, request, slug, project_id):
issue_property, created = IssueProperty.objects.get_or_create( issue_property = IssueProperty.objects.get(
user=request.user, user=request.user,
project_id=project_id, project_id=project_id,
) )
if not created: issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.properties = request.data.get("properties", {}) issue_property.display_filters = request.data.get(
issue_property.save() "display_filters", issue_property.display_filters
issue_property.properties = request.data.get("properties", {}) )
issue_property.display_properties = request.data.get(
"display_properties", issue_property.display_properties
)
issue_property.save() issue_property.save()
serializer = IssuePropertySerializer(issue_property) serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -708,6 +719,13 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_reactions", "issue_reactions",
@ -728,7 +746,7 @@ class SubIssuesEndpoint(BaseAPIView):
item["state_group"]: item["state_count"] for item in state_distribution item["state_group"]: item["state_count"] for item in state_distribution
} }
serializer = IssueLiteSerializer( serializer = IssueSerializer(
sub_issues, sub_issues,
many=True, many=True,
) )
@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView):
] ]
return Response( return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data, IssueSerializer(updated_sub_issues, many=True).data,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -1062,9 +1080,10 @@ class IssueArchiveViewSet(BaseViewSet):
else issue_queryset.filter(parent__isnull=True) else issue_queryset.filter(parent__isnull=True)
) )
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data issues = IssueLiteSerializer(
issue_dict = {str(issue["id"]): issue for issue in issues} issue_queryset, many=True, fields=fields if fields else None
return Response(issue_dict, status=status.HTTP_200_OK) ).data
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(
@ -1365,23 +1384,62 @@ class IssueRelationViewSet(BaseViewSet):
.distinct() .distinct()
) )
def list(self, request, slug, project_id, issue_id):
issue_relations = (
IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("issue")
.order_by("-created_at")
.distinct()
)
blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id)
blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id)
duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate")
duplicate_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="duplicate")
relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to")
relates_to_issues_related = issue_relations.filter(related_issue_id=issue_id, relation_type="relates_to")
blocked_by_issues_serialized = IssueRelationSerializer(blocked_by_issues, many=True).data
duplicate_issues_serialized = IssueRelationSerializer(duplicate_issues, many=True).data
relates_to_issues_serialized = IssueRelationSerializer(relates_to_issues, many=True).data
# revere relation for blocked by issues
blocking_issues_serialized = RelatedIssueSerializer(blocking_issues, many=True).data
# reverse relation for duplicate issues
duplicate_issues_related_serialized = RelatedIssueSerializer(duplicate_issues_related, many=True).data
# reverse relation for related issues
relates_to_issues_related_serialized = RelatedIssueSerializer(relates_to_issues_related, many=True).data
response_data = {
'blocking': blocking_issues_serialized,
'blocked_by': blocked_by_issues_serialized,
'duplicate': duplicate_issues_serialized + duplicate_issues_related_serialized,
'relates_to': relates_to_issues_serialized + relates_to_issues_related_serialized,
}
return Response(response_data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
related_list = request.data.get("related_list", []) relation_type = request.data.get("relation_type", None)
relation = request.data.get("relation", None) issues = request.data.get("issues", [])
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
issue_relation = IssueRelation.objects.bulk_create( issue_relation = IssueRelation.objects.bulk_create(
[ [
IssueRelation( IssueRelation(
issue_id=related_issue["issue"], issue_id=issue if relation_type == "blocking" else issue_id,
related_issue_id=related_issue["related_issue"], related_issue_id=issue_id if relation_type == "blocking" else issue,
relation_type=related_issue["relation_type"], relation_type="blocked_by" if relation_type == "blocking" else relation_type,
project_id=project_id, project_id=project_id,
workspace_id=project.workspace_id, workspace_id=project.workspace_id,
created_by=request.user, created_by=request.user,
updated_by=request.user, updated_by=request.user,
) )
for related_issue in related_list for issue in issues
], ],
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
@ -1397,7 +1455,7 @@ class IssueRelationViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
) )
if relation == "blocking": if relation_type == "blocking":
return Response( return Response(
RelatedIssueSerializer(issue_relation, many=True).data, RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
@ -1408,10 +1466,18 @@ class IssueRelationViewSet(BaseViewSet):
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
def destroy(self, request, slug, project_id, issue_id, pk): def remove_relation(self, request, slug, project_id, issue_id):
issue_relation = IssueRelation.objects.get( relation_type = request.data.get("relation_type", None)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk related_issue = request.data.get("related_issue", None)
)
if relation_type == "blocking":
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=related_issue, related_issue_id=issue_id
)
else:
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, related_issue_id=related_issue
)
current_instance = json.dumps( current_instance = json.dumps(
IssueRelationSerializer(issue_relation).data, IssueRelationSerializer(issue_relation).data,
cls=DjangoJSONEncoder, cls=DjangoJSONEncoder,
@ -1419,7 +1485,7 @@ class IssueRelationViewSet(BaseViewSet):
issue_relation.delete() issue_relation.delete()
issue_activity.delay( issue_activity.delay(
type="issue_relation.activity.deleted", type="issue_relation.activity.deleted",
requested_data=json.dumps({"related_list": None}), requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),
@ -1547,9 +1613,10 @@ class IssueDraftViewSet(BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data issues = IssueLiteSerializer(
issue_dict = {str(issue["id"]): issue for issue in issues} issue_queryset, many=True, fields=fields if fields else None
return Response(issue_dict, status=status.HTTP_200_OK) ).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
@ -1626,4 +1693,4 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=current_instance, current_instance=current_instance,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
) )
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -21,8 +21,9 @@ from plane.app.serializers import (
ModuleLinkSerializer, ModuleLinkSerializer,
ModuleFavoriteSerializer, ModuleFavoriteSerializer,
IssueStateSerializer, IssueStateSerializer,
ModuleUserPropertiesSerializer,
) )
from plane.app.permissions import ProjectEntityPermission from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import ( from plane.db.models import (
Module, Module,
ModuleIssue, ModuleIssue,
@ -32,6 +33,7 @@ from plane.db.models import (
ModuleFavorite, ModuleFavorite,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
ModuleUserProperties,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
) )
def get_queryset(self): def get_queryset(self):
subquery = ModuleFavorite.objects.filter( subquery = ModuleFavorite.objects.filter(
user=self.request.user, user=self.request.user,
module_id=OuterRef("pk"), module_id=OuterRef("pk"),
@ -136,7 +137,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
), ),
) )
) )
.order_by("-is_favorite","-created_at") .order_by("-is_favorite", "-created_at")
) )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def list(self, request, slug, project_id):
queryset = self.get_queryset()
fields = [field for field in request.GET.get("fields", "").split(",") if field]
modules = ModuleSerializer(
queryset, many=True, fields=fields if fields else None
).data
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk): def retrieve(self, request, slug, project_id, pk):
queryset = self.get_queryset().get(pk=pk) queryset = self.get_queryset().get(pk=pk)
@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
webhook_event = "module_issue" webhook_event = "module_issue"
bulk = True bulk = True
filterset_fields = [ filterset_fields = [
"issue__labels__id", "issue__labels__id",
"issue__assignees__id", "issue__assignees__id",
@ -335,7 +343,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.annotate(bridge_id=F("issue_module__id"))
.filter(project_id=project_id) .filter(project_id=project_id)
.filter(workspace__slug=slug) .filter(workspace__slug=slug)
.select_related("project") .select_related("project")
@ -359,9 +366,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count") .values("count")
) )
) )
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data serializer = IssueStateSerializer(
issue_dict = {str(issue["id"]): issue for issue in issues} issues, many=True, fields=fields if fields else None
return Response(issue_dict, status=status.HTTP_200_OK) )
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, module_id): def create(self, request, slug, project_id, module_id):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
@ -444,20 +452,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
def destroy(self, request, slug, project_id, module_id, pk): def destroy(self, request, slug, project_id, module_id, issue_id):
module_issue = ModuleIssue.objects.get( module_issue = ModuleIssue.objects.get(
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk workspace__slug=slug,
project_id=project_id,
module_id=module_id,
issue_id=issue_id,
) )
issue_activity.delay( issue_activity.delay(
type="module.activity.deleted", type="module.activity.deleted",
requested_data=json.dumps( requested_data=json.dumps(
{ {
"module_id": str(module_id), "module_id": str(module_id),
"issues": [str(module_issue.issue_id)], "issues": [str(issue_id)],
} }
), ),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(module_issue.issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),
current_instance=None, current_instance=None,
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
@ -521,4 +532,42 @@ class ModuleFavoriteViewSet(BaseViewSet):
module_id=module_id, module_id=module_id,
) )
module_favorite.delete() module_favorite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def patch(self, request, slug, project_id, module_id):
module_properties = ModuleUserProperties.objects.get(
user=request.user,
module_id=module_id,
project_id=project_id,
workspace__slug=slug,
)
module_properties.filters = request.data.get(
"filters", module_properties.filters
)
module_properties.display_filters = request.data.get(
"display_filters", module_properties.display_filters
)
module_properties.display_properties = request.data.get(
"display_properties", module_properties.display_properties
)
module_properties.save()
serializer = ModuleUserPropertiesSerializer(module_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug, project_id, module_id):
module_properties, _ = ModuleUserProperties.objects.get_or_create(
user=request.user,
project_id=project_id,
module_id=module_id,
workspace__slug=slug,
)
serializer = ModuleUserPropertiesSerializer(module_properties)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -157,9 +157,8 @@ class PageViewSet(BaseViewSet):
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
queryset = self.get_queryset().filter(archived_at__isnull=True) queryset = self.get_queryset().filter(archived_at__isnull=True)
return Response( pages = PageSerializer(queryset, many=True).data
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK return Response(pages, status=status.HTTP_200_OK)
)
def archive(self, request, slug, project_id, page_id): def archive(self, request, slug, project_id, page_id):
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
@ -210,9 +209,9 @@ class PageViewSet(BaseViewSet):
workspace__slug=slug, workspace__slug=slug,
).filter(archived_at__isnull=False) ).filter(archived_at__isnull=False)
return Response( pages = PageSerializer(pages, many=True).data
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK return Response(pages, status=status.HTTP_200_OK)
)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)

View File

@ -36,6 +36,7 @@ from plane.app.serializers import (
ProjectFavoriteSerializer, ProjectFavoriteSerializer,
ProjectDeployBoardSerializer, ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer, ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
) )
from plane.app.permissions import ( from plane.app.permissions import (
@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects, many=True projects, many=True
).data, ).data,
) )
projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
return Response(projects, status=status.HTTP_200_OK)
return Response(
ProjectListSerializer(
projects, many=True, fields=fields if fields else None
).data
)
def create(self, request, slug): def create(self, request, slug):
try: try:
@ -713,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get( # Get the list of project members for the project
member=request.user,
workspace__slug=slug,
project_id=project_id,
is_active=True,
)
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
@ -727,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True, is_active=True,
).select_related("project", "member", "workspace") ).select_related("project", "member", "workspace")
if project_member.role > 10: serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
files = [] files = []
s3_client_params = { s3 = boto3.client(
"service_name": "s3", "s3",
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID, aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
"aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
} )
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
s3 = boto3.client(**s3_client_params)
params = { params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/", "Prefix": "static/project-cover/",
@ -1034,19 +1016,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
if not content["Key"].endswith( if not content["Key"].endswith(
"/" "/"
): # This line ensures we're only getting files, not "sub-folders" ): # This line ensures we're only getting files, not "sub-folders"
if ( files.append(
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN") f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
and settings.AWS_S3_CUSTOM_DOMAIN )
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
and settings.AWS_S3_URL_PROTOCOL
):
files.append(
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
)
else:
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
return Response(files, status=status.HTTP_200_OK) return Response(files, status=status.HTTP_200_OK)

View File

@ -27,7 +27,12 @@ from plane.app.serializers import (
IssueLiteSerializer, IssueLiteSerializer,
IssueViewFavoriteSerializer, IssueViewFavoriteSerializer,
) )
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
)
from plane.db.models import ( from plane.db.models import (
Workspace, Workspace,
GlobalView, GlobalView,
@ -43,8 +48,8 @@ from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet): class GlobalViewViewSet(BaseViewSet):
serializer_class = GlobalViewSerializer serializer_class = IssueViewSerializer
model = GlobalView model = IssueView
permission_classes = [ permission_classes = [
WorkspaceEntityPermission, WorkspaceEntityPermission,
] ]
@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.select_related("workspace") .select_related("workspace")
.order_by(self.request.GET.get("order_by", "-created_at")) .order_by(self.request.GET.get("order_by", "-created_at"))
.distinct() .distinct()
@ -179,12 +185,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data serializer = IssueLiteSerializer(
issue_dict = {str(issue["id"]): issue for issue in issues} issue_queryset, many=True, fields=fields if fields else None
return Response(
issue_dict,
status=status.HTTP_200_OK,
) )
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet): class IssueViewViewSet(BaseViewSet):
@ -217,6 +221,14 @@ class IssueViewViewSet(BaseViewSet):
.distinct() .distinct()
) )
def list(self, request, slug, project_id):
queryset = self.get_queryset()
fields = [field for field in request.GET.get("fields", "").split(",") if field]
views = IssueViewSerializer(
queryset, many=True, fields=fields if fields else None
).data
return Response(views, status=status.HTTP_200_OK)
class IssueViewFavoriteViewSet(BaseViewSet): class IssueViewFavoriteViewSet(BaseViewSet):
serializer_class = IssueViewFavoriteSerializer serializer_class = IssueViewFavoriteSerializer
@ -246,4 +258,4 @@ class IssueViewFavoriteViewSet(BaseViewSet):
view_id=view_id, view_id=view_id,
) )
view_favourite.delete() view_favourite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -44,6 +44,8 @@ from plane.app.serializers import (
IssueLiteSerializer, IssueLiteSerializer,
WorkspaceMemberAdminSerializer, WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer, WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer,
) )
from plane.app.views.base import BaseAPIView from plane.app.views.base import BaseAPIView
from . import BaseViewSet from . import BaseViewSet
@ -64,6 +66,7 @@ from plane.db.models import (
WorkspaceMember, WorkspaceMember,
CycleIssue, CycleIssue,
IssueReaction, IssueReaction,
WorkspaceUserProperties
) )
from plane.app.permissions import ( from plane.app.permissions import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
@ -71,11 +74,13 @@ from plane.app.permissions import (
WorkspaceEntityPermission, WorkspaceEntityPermission,
WorkspaceViewerPermission, WorkspaceViewerPermission,
WorkspaceUserPermission, WorkspaceUserPermission,
ProjectLitePermission,
) )
from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.bgtasks.event_tracking_task import workspace_invite_event from plane.bgtasks.event_tracking_task import workspace_invite_event
class WorkSpaceViewSet(BaseViewSet): class WorkSpaceViewSet(BaseViewSet):
model = Workspace model = Workspace
serializer_class = WorkSpaceSerializer serializer_class = WorkSpaceSerializer
@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
] ]
def get(self, request): def get(self, request):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
member_count = ( member_count = (
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(
workspace=OuterRef("id"), workspace=OuterRef("id"),
@ -208,9 +214,12 @@ class UserWorkSpacesEndpoint(BaseAPIView):
) )
.distinct() .distinct()
) )
workspaces = WorkSpaceSerializer(
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True) self.filter_queryset(workspace),
return Response(serializer.data, status=status.HTTP_200_OK) fields=fields if fields else None,
many=True,
).data
return Response(workspaces, status=status.HTTP_200_OK)
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView): class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
@ -407,7 +416,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
# Delete the invitation # Delete the invitation
workspace_invite.delete() workspace_invite.delete()
# Send event # Send event
workspace_invite_event.delay( workspace_invite_event.delay(
user=user.id if user is not None else None, user=user.id if user is not None else None,
@ -537,10 +546,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_members = self.get_queryset() workspace_members = self.get_queryset()
if workspace_member.role > 10: if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True) serializer = WorkspaceMemberAdminSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
else: else:
serializer = WorkSpaceMemberSerializer( serializer = WorkSpaceMemberSerializer(
workspace_members, workspace_members,
fields=("id", "member", "role"),
many=True, many=True,
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -705,6 +719,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class WorkspaceProjectMemberEndpoint(BaseAPIView):
serializer_class = ProjectMemberRoleSerializer
model = ProjectMember
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
# Fetch all project IDs where the user is involved
project_ids = ProjectMember.objects.filter(
member=request.user,
member__is_bot=False,
is_active=True,
).values_list('project_id', flat=True).distinct()
# Get all the project members in which the user is involved
project_members = ProjectMember.objects.filter(
workspace__slug=slug,
member__is_bot=False,
project_id__in=project_ids,
is_active=True,
).select_related("project", "member", "workspace")
project_members = ProjectMemberRoleSerializer(project_members, many=True).data
project_members_dict = dict()
# Construct a dictionary with project_id as key and project_members as value
for project_member in project_members:
project_id = project_member.pop("project")
if str(project_id) not in project_members_dict:
project_members_dict[str(project_id)] = []
project_members_dict[str(project_id)].append(project_member)
return Response(project_members_dict, status=status.HTTP_200_OK)
class TeamMemberViewSet(BaseViewSet): class TeamMemberViewSet(BaseViewSet):
serializer_class = TeamSerializer serializer_class = TeamSerializer
model = Team model = Team
@ -1334,8 +1385,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
issues = IssueLiteSerializer( issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None issue_queryset, many=True, fields=fields if fields else None
).data ).data
issue_dict = {str(issue["id"]): issue for issue in issues} return Response(issues, status=status.HTTP_200_OK)
return Response(issue_dict, status=status.HTTP_200_OK)
class WorkspaceLabelsEndpoint(BaseAPIView): class WorkspaceLabelsEndpoint(BaseAPIView):
@ -1349,3 +1399,30 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
).values("parent", "name", "color", "id", "project_id", "workspace__slug") ).values("parent", "name", "color", "id", "project_id", "workspace__slug")
return Response(labels, status=status.HTTP_200_OK) return Response(labels, status=status.HTTP_200_OK)
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceViewerPermission,
]
def patch(self, request, slug):
workspace_properties = WorkspaceUserProperties.objects.get(
user=request.user,
workspace__slug=slug,
)
workspace_properties.filters = request.data.get("filters", workspace_properties.filters)
workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters)
workspace_properties.display_properties = request.data.get("display_properties", workspace_properties.display_properties)
workspace_properties.save()
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def get(self, request, slug):
workspace_properties, _ = WorkspaceUserProperties.objects.get_or_create(
user=request.user, workspace__slug=slug
)
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -112,8 +112,16 @@ def track_parent(
epoch, epoch,
): ):
if current_instance.get("parent") != requested_data.get("parent"): if current_instance.get("parent") != requested_data.get("parent"):
old_parent = Issue.objects.filter(pk=current_instance.get("parent")).first() if current_instance.get("parent") is not None else None old_parent = (
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None Issue.objects.filter(pk=current_instance.get("parent")).first()
if current_instance.get("parent") is not None
else None
)
new_parent = (
Issue.objects.filter(pk=requested_data.get("parent")).first()
if requested_data.get("parent") is not None
else None
)
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
@ -714,7 +722,9 @@ def create_cycle_issue_activity(
cycle = Cycle.objects.filter( cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle") pk=created_record.get("fields").get("cycle")
).first() ).first()
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue: if issue:
issue.updated_at = timezone.now() issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"]) issue.save(update_fields=["updated_at"])
@ -830,7 +840,9 @@ def create_module_issue_activity(
module = Module.objects.filter( module = Module.objects.filter(
pk=created_record.get("fields").get("module") pk=created_record.get("fields").get("module")
).first() ).first()
issue = Issue.objects.filter(pk=created_record.get("fields").get("issue")).first() issue = Issue.objects.filter(
pk=created_record.get("fields").get("issue")
).first()
if issue: if issue:
issue.updated_at = timezone.now() issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"]) issue.save(update_fields=["updated_at"])
@ -1276,40 +1288,42 @@ def create_issue_relation_activity(
current_instance = ( current_instance = (
json.loads(current_instance) if current_instance is not None else None json.loads(current_instance) if current_instance is not None else None
) )
if current_instance is None and requested_data.get("related_list") is not None: if current_instance is None and requested_data.get("issues") is not None:
for issue_relation in requested_data.get("related_list"): for related_issue in requested_data.get("issues"):
if issue_relation.get("relation_type") == "blocked_by": issue = Issue.objects.get(pk=related_issue)
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
issue_id=issue_relation.get("related_issue"), issue_id=issue_id,
actor_id=actor_id, actor_id=actor_id,
verb="created", verb="created",
old_value="", old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}", new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=relation_type, field=requested_data.get("relation_type"),
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
comment=f"added {relation_type} relation", comment=f"added {requested_data.get('relation_type')} relation",
old_identifier=issue_relation.get("issue"), old_identifier=related_issue,
) )
) )
issue = Issue.objects.get(pk=issue_relation.get("related_issue")) issue = Issue.objects.get(pk=issue_id)
issue_activities.append( issue_activities.append(
IssueActivity( IssueActivity(
issue_id=issue_relation.get("issue"), issue_id=related_issue,
actor_id=actor_id, actor_id=actor_id,
verb="created", verb="created",
old_value="", old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}", new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=f'{issue_relation.get("relation_type")}', field="blocking"
if requested_data.get("relation_type") == "blocked_by"
else (
"blocked_by"
if requested_data.get("relation_type") == "blocking"
else requested_data.get("relation_type")
),
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
comment=f'added {issue_relation.get("relation_type")} relation', comment=f'added {"blocking" if requested_data.get("relation_type") == "blocked_by" else ("blocked_by" if requested_data.get("relation_type") == "blocking" else requested_data.get("relation_type")),} relation',
old_identifier=issue_relation.get("related_issue"), old_identifier=issue_id,
epoch=epoch, epoch=epoch,
) )
) )
@ -1329,44 +1343,44 @@ def delete_issue_relation_activity(
current_instance = ( current_instance = (
json.loads(current_instance) if current_instance is not None else None json.loads(current_instance) if current_instance is not None else None
) )
if current_instance is not None and requested_data.get("related_list") is None: issue = Issue.objects.get(pk=requested_data.get("related_issue"))
if current_instance.get("relation_type") == "blocked_by": issue_activities.append(
relation_type = "blocking" IssueActivity(
else: issue_id=issue_id,
relation_type = current_instance.get("relation_type") actor_id=actor_id,
issue = Issue.objects.get(pk=current_instance.get("issue")) verb="deleted",
issue_activities.append( old_value=f"{issue.project.identifier}-{issue.sequence_id}",
IssueActivity( new_value="",
issue_id=current_instance.get("related_issue"), field=requested_data.get("relation_type"),
actor_id=actor_id, project_id=project_id,
verb="deleted", workspace_id=workspace_id,
old_value=f"{issue.project.identifier}-{issue.sequence_id}", comment=f"deleted {requested_data.get('relation_type')} relation",
new_value="", old_identifier=requested_data.get("related_issue"),
field=relation_type, epoch=epoch,
project_id=project_id,
workspace_id=workspace_id,
comment=f"deleted {relation_type} relation",
old_identifier=current_instance.get("issue"),
epoch=epoch,
)
) )
issue = Issue.objects.get(pk=current_instance.get("related_issue")) )
issue_activities.append( issue = Issue.objects.get(pk=issue_id)
IssueActivity( issue_activities.append(
issue_id=current_instance.get("issue"), IssueActivity(
actor_id=actor_id, issue_id=requested_data.get("related_issue"),
verb="deleted", actor_id=actor_id,
old_value=f"{issue.project.identifier}-{issue.sequence_id}", verb="deleted",
new_value="", old_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=f'{current_instance.get("relation_type")}', new_value="",
project_id=project_id, field="blocking"
workspace_id=workspace_id, if requested_data.get("relation_type") == "blocked_by"
comment=f'deleted {current_instance.get("relation_type")} relation', else (
old_identifier=current_instance.get("related_issue"), "blocked_by"
epoch=epoch, if requested_data.get("relation_type") == "blocking"
) else requested_data.get("relation_type")
),
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {requested_data.get("relation_type")} relation',
old_identifier=requested_data.get("related_issue"),
epoch=epoch,
) )
)
def create_draft_issue_activity( def create_draft_issue_activity(
requested_data, requested_data,

View File

@ -291,6 +291,9 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
sender = "in_app:issue_activities:assigned" sender = "in_app:issue_activities:assigned"
for issue_activity in issue_activities_created: for issue_activity in issue_activities_created:
# Do not send notification for description update
if issue_activity.get("field") == "description":
continue;
issue_comment = issue_activity.get("issue_comment") issue_comment = issue_activity.get("issue_comment")
if issue_comment is not None: if issue_comment is not None:
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(
@ -341,7 +344,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
.order_by("-created_at") .order_by("-created_at")
.first() .first()
) )
actor = User.objects.get(pk=actor_id) actor = User.objects.get(pk=actor_id)
for mention_id in comment_mentions: for mention_id in comment_mentions:

View File

@ -0,0 +1,136 @@
# Generated by Django 4.2.7 on 2023-12-20 11:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import plane.db.models.cycle
import plane.db.models.issue
import plane.db.models.module
import plane.db.models.view
import plane.db.models.workspace
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0050_user_use_case_alter_workspace_organization_size'),
]
operations = [
migrations.RenameField(
model_name='issueview',
old_name='query_data',
new_name='filters',
),
migrations.RenameField(
model_name='issueproperty',
old_name='properties',
new_name='display_properties',
),
migrations.AlterField(
model_name='issueproperty',
name='display_properties',
field=models.JSONField(default=plane.db.models.issue.get_default_display_properties),
),
migrations.AddField(
model_name='issueproperty',
name='display_filters',
field=models.JSONField(default=plane.db.models.issue.get_default_display_filters),
),
migrations.AddField(
model_name='issueproperty',
name='filters',
field=models.JSONField(default=plane.db.models.issue.get_default_filters),
),
migrations.AddField(
model_name='issueview',
name='display_filters',
field=models.JSONField(default=plane.db.models.view.get_default_display_filters),
),
migrations.AddField(
model_name='issueview',
name='display_properties',
field=models.JSONField(default=plane.db.models.view.get_default_display_properties),
),
migrations.AddField(
model_name='issueview',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AlterField(
model_name='issueview',
name='project',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project'),
),
migrations.CreateModel(
name='WorkspaceUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.workspace.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.workspace.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.workspace.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_properties', to='db.workspace')),
],
options={
'verbose_name': 'Workspace User Property',
'verbose_name_plural': 'Workspace User Property',
'db_table': 'Workspace_user_properties',
'ordering': ('-created_at',),
'unique_together': {('workspace', 'user')},
},
),
migrations.CreateModel(
name='ModuleUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.module.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.module.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.module.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to='db.module')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Module User Property',
'verbose_name_plural': 'Module User Property',
'db_table': 'module_user_properties',
'ordering': ('-created_at',),
'unique_together': {('module', 'user')},
},
),
migrations.CreateModel(
name='CycleUserProperties',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('filters', models.JSONField(default=plane.db.models.cycle.get_default_filters)),
('display_filters', models.JSONField(default=plane.db.models.cycle.get_default_display_filters)),
('display_properties', models.JSONField(default=plane.db.models.cycle.get_default_display_properties)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('cycle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to='db.cycle')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cycle_user_properties', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
],
options={
'verbose_name': 'Cycle User Property',
'verbose_name_plural': 'Cycle User Properties',
'db_table': 'cycle_user_properties',
'ordering': ('-created_at',),
'unique_together': {('cycle', 'user')},
},
),
]

View File

@ -0,0 +1,65 @@
# Generated by Django 4.2.7 on 2023-12-19 19:11
from plane.db.models import WorkspaceUserProperties, ProjectMember, IssueView
from django.db import migrations
def workspace_user_properties(apps, schema_editor):
WorkspaceMember = apps.get_model("db", "WorkspaceMember")
updated_workspace_user_properties = []
for workspace_members in WorkspaceMember.objects.all():
updated_workspace_user_properties.append(
WorkspaceUserProperties(
user_id=workspace_members.member_id,
display_filters=workspace_members.view_props.get("display_filters"),
display_properties=workspace_members.view_props.get("display_properties"),
workspace_id=workspace_members.workspace_id,
)
)
WorkspaceUserProperties.objects.bulk_create(updated_workspace_user_properties, batch_size=2000)
def project_user_properties(apps, schema_editor):
IssueProperty = apps.get_model("db", "IssueProperty")
updated_issue_user_properties = []
for issue_property in IssueProperty.objects.all():
project_member = ProjectMember.objects.filter(project_id=issue_property.project_id, member_id=issue_property.user_id).first()
if project_member:
issue_property.filters = project_member.view_props.get("filters")
issue_property.display_filters = project_member.view_props.get("display_filters")
updated_issue_user_properties.append(issue_property)
IssueProperty.objects.bulk_update(updated_issue_user_properties, ["filters", "display_filters"], batch_size=2000)
def issue_view(apps, schema_editor):
GlobalView = apps.get_model("db", "GlobalView")
updated_issue_views = []
for global_view in GlobalView.objects.all():
updated_issue_views.append(
IssueView(
workspace_id=global_view.workspace_id,
name=global_view.name,
description=global_view.description,
query=global_view.query,
access=global_view.access,
filters=global_view.query_data.get("filters", {}),
sort_order=global_view.sort_order,
created_by_id=global_view.created_by_id,
updated_by_id=global_view.updated_by_id,
)
)
IssueView.objects.bulk_create(updated_issue_views, batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0051_remove_issueproperty_properties_and_more'),
]
operations = [
migrations.RunPython(workspace_user_properties),
migrations.RunPython(project_user_properties),
migrations.RunPython(issue_view),
]

View File

@ -9,6 +9,8 @@ from .workspace import (
WorkspaceMemberInvite, WorkspaceMemberInvite,
TeamMember, TeamMember,
WorkspaceTheme, WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceBaseModel,
) )
from .project import ( from .project import (
@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection
from .state import State from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
from .view import GlobalView, IssueView, IssueViewFavorite from .view import GlobalView, IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite, ModuleUserProperties
from .api import APIToken, APIActivityLog from .api import APIToken, APIActivityLog

View File

@ -6,6 +6,47 @@ from django.conf import settings
from . import ProjectBaseModel from . import ProjectBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class Cycle(ProjectBaseModel): class Cycle(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Cycle Name") name = models.CharField(max_length=255, verbose_name="Cycle Name")
description = models.TextField(verbose_name="Cycle Description", blank=True) description = models.TextField(verbose_name="Cycle Description", blank=True)
@ -89,3 +130,28 @@ class CycleFavorite(ProjectBaseModel):
def __str__(self): def __str__(self):
"""Return user and the cycle""" """Return user and the cycle"""
return f"{self.user.email} <{self.cycle.name}>" return f"{self.user.email} <{self.cycle.name}>"
class CycleUserProperties(ProjectBaseModel):
cycle = models.ForeignKey(
"db.Cycle", on_delete=models.CASCADE, related_name="cycle_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="cycle_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["cycle", "user"]
verbose_name = "Cycle User Property"
verbose_name_plural = "Cycle User Properties"
db_table = "cycle_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.cycle.name} {self.user.email}"

View File

@ -33,6 +33,48 @@ def get_default_properties():
} }
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
# TODO: Handle identifiers for Bulk Inserts - nk # TODO: Handle identifiers for Bulk Inserts - nk
class IssueManager(models.Manager): class IssueManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -394,7 +436,9 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="issue_property_user", related_name="issue_property_user",
) )
properties = models.JSONField(default=get_default_properties) filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta: class Meta:
verbose_name = "Issue Property" verbose_name = "Issue Property"

View File

@ -6,6 +6,47 @@ from django.conf import settings
from . import ProjectBaseModel from . import ProjectBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class Module(ProjectBaseModel): class Module(ProjectBaseModel):
name = models.CharField(max_length=255, verbose_name="Module Name") name = models.CharField(max_length=255, verbose_name="Module Name")
description = models.TextField(verbose_name="Module Description", blank=True) description = models.TextField(verbose_name="Module Description", blank=True)
@ -141,3 +182,28 @@ class ModuleFavorite(ProjectBaseModel):
def __str__(self): def __str__(self):
"""Return user and the module""" """Return user and the module"""
return f"{self.user.email} <{self.module.name}>" return f"{self.user.email} <{self.module.name}>"
class ModuleUserProperties(ProjectBaseModel):
module = models.ForeignKey(
"db.Module", on_delete=models.CASCADE, related_name="module_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="module_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["module", "user"]
verbose_name = "Module User Property"
verbose_name_plural = "Module User Property"
db_table = "module_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.module.name} {self.user.email}"

View File

@ -3,9 +3,50 @@ from django.db import models
from django.conf import settings from django.conf import settings
# Module import # Module import
from . import ProjectBaseModel, BaseModel from . import ProjectBaseModel, BaseModel, WorkspaceBaseModel
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
}
def get_default_display_properties():
return {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
}
class GlobalView(BaseModel): class GlobalView(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views" "db.Workspace", on_delete=models.CASCADE, related_name="global_views"
@ -40,14 +81,17 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>" return f"{self.name} <{self.workspace.name}>"
class IssueView(ProjectBaseModel): class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name") name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True) description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query") query = models.JSONField(verbose_name="View Query")
filters = models.JSONField(default=dict)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
access = models.PositiveSmallIntegerField( access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public")) default=1, choices=((0, "Private"), (1, "Public"))
) )
query_data = models.JSONField(default=dict) sort_order = models.FloatField(default=65535)
class Meta: class Meta:
verbose_name = "Issue View" verbose_name = "Issue View"

View File

@ -54,6 +54,51 @@ def get_default_props():
}, },
} }
def get_default_filters():
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
return {
"display_filters": {
"group_by": None,
"order_by": "-created_at",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
}
def get_default_display_properties():
return {
"display_properties": {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"updated_on": True,
},
}
def get_issue_props(): def get_issue_props():
return { return {
@ -103,6 +148,22 @@ class Workspace(BaseModel):
ordering = ("-created_at",) ordering = ("-created_at",)
class WorkspaceBaseModel(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", models.CASCADE, related_name="workspace_%(class)s"
)
project = models.ForeignKey(
"db.Project", models.CASCADE, related_name="project_%(class)s", null=True
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.project:
self.workspace = self.project.workspace
super(WorkspaceBaseModel, self).save(*args, **kwargs)
class WorkspaceMember(BaseModel): class WorkspaceMember(BaseModel):
workspace = models.ForeignKey( workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member" "db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
@ -218,3 +279,28 @@ class WorkspaceTheme(BaseModel):
verbose_name_plural = "Workspace Themes" verbose_name_plural = "Workspace Themes"
db_table = "workspace_themes" db_table = "workspace_themes"
ordering = ("-created_at",) ordering = ("-created_at",)
class WorkspaceUserProperties(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_user_properties"
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="workspace_user_properties",
)
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
class Meta:
unique_together = ["workspace", "user"]
verbose_name = "Workspace User Property"
verbose_name_plural = "Workspace User Property"
db_table = "Workspace_user_properties"
ordering = ("-created_at",)
def __str__(self):
return f"{self.workspace.name} {self.user.email}"

View File

@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
dj-database-url==2.1.0 dj-database-url==2.1.0
posthog==3.0.2 posthog==3.0.2
cryptography==41.0.6 cryptography==41.0.5
lxml==4.9.3 lxml==4.9.3
boto3==1.28.40 boto3==1.28.40

View File

@ -39,7 +39,7 @@ function download(){
echo "" echo ""
echo "Latest version is now available for you to use" echo "Latest version is now available for you to use"
echo "" echo ""
echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo "" echo ""
} }

View File

@ -10,7 +10,8 @@
"packages/eslint-config-custom", "packages/eslint-config-custom",
"packages/tailwind-config-custom", "packages/tailwind-config-custom",
"packages/tsconfig", "packages/tsconfig",
"packages/ui" "packages/ui",
"packages/types"
], ],
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",

View File

@ -0,0 +1,7 @@
{
"name": "@plane/types",
"version": "0.14.0",
"private": true,
"main": "./src/index.d.ts"
}

View File

@ -1,4 +1,4 @@
import { IProjectLite, IWorkspaceLite } from "types"; import { IProjectLite, IWorkspaceLite } from "@plane/types";
export interface IGptResponse { export interface IGptResponse {
response: string; response: string;

View File

@ -1,6 +1,4 @@
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
export interface IAppConfig { export interface IAppConfig {
email_password_login: boolean; email_password_login: boolean;

View File

@ -1,4 +1,4 @@
import type { IUser, IIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "types"; import type { IUser, TIssue, IProjectLite, IWorkspaceLite, IIssueFilterOptions, IUserLite } from "@plane/types";
export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft"; export type TCycleView = "all" | "active" | "upcoming" | "completed" | "draft";
@ -68,7 +68,7 @@ export type TLabelsDistribution = {
export interface CycleIssueResponse { export interface CycleIssueResponse {
id: string; id: string;
issue_detail: IIssue; issue_detail: TIssue;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
created_by: string; created_by: string;
@ -82,7 +82,7 @@ export interface CycleIssueResponse {
export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectCycleType = (ICycle & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | null; export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | null;
export type CycleDateCheckData = { export type CycleDateCheckData = {
start_date: string; start_date: string;

View File

@ -1,24 +1,24 @@
export interface IEstimate { export interface IEstimate {
id: string;
created_at: Date; created_at: Date;
updated_at: Date;
name: string;
description: string;
created_by: string; created_by: string;
updated_by: string; description: string;
points: IEstimatePoint[]; id: string;
name: string;
project: string; project: string;
project_detail: IProject; project_detail: IProject;
updated_at: Date;
updated_by: string;
points: IEstimatePoint[];
workspace: string; workspace: string;
workspace_detail: IWorkspace; workspace_detail: IWorkspace;
} }
export interface IEstimatePoint { export interface IEstimatePoint {
id: string;
created_at: string; created_at: string;
created_by: string; created_by: string;
description: string; description: string;
estimate: string; estimate: string;
id: string;
key: number; key: number;
project: string; project: string;
updated_at: string; updated_at: string;

View File

@ -1,9 +1,9 @@
export * from "./github-importer"; export * from "./github-importer";
export * from "./jira-importer"; export * from "./jira-importer";
import { IProjectLite } from "types/projects"; import { IProjectLite } from "../projects";
// types // types
import { IUserLite } from "types/users"; import { IUserLite } from "../users";
export interface IImporterService { export interface IImporterService {
created_at: string; created_at: string;

View File

@ -1,7 +1,7 @@
import { IIssue } from "./issues"; import { TIssue } from "./issues";
import type { IProjectLite } from "./projects"; import type { IProjectLite } from "./projects";
export interface IInboxIssue extends IIssue { export interface IInboxIssue extends TIssue {
issue_inbox: { issue_inbox: {
duplicate_to: string | null; duplicate_to: string | null;
id: string; id: string;

View File

@ -21,6 +21,11 @@ export * from "./reaction";
export * from "./view-props"; export * from "./view-props";
export * from "./workspace-views"; export * from "./workspace-views";
export * from "./webhook"; export * from "./webhook";
export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable
export * from "./auth";
export * from "./api_token";
export * from "./instance";
export * from "./app";
export type NestedKeyOf<ObjectType extends object> = { export type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object

View File

@ -1,3 +1,4 @@
import { ReactElement } from "react";
import { KeyedMutator } from "swr"; import { KeyedMutator } from "swr";
import type { import type {
IState, IState,
@ -10,7 +11,8 @@ import type {
IStateLite, IStateLite,
Properties, Properties,
IIssueDisplayFilterOptions, IIssueDisplayFilterOptions,
} from "types"; IIssueReaction,
} from "@plane/types";
export interface IIssueCycle { export interface IIssueCycle {
id: string; id: string;
@ -83,6 +85,7 @@ export interface IIssue {
attachment_count: number; attachment_count: number;
attachments: any[]; attachments: any[];
issue_relations: IssueRelation[]; issue_relations: IssueRelation[];
issue_reactions: IIssueReaction[];
related_issues: IssueRelation[]; related_issues: IssueRelation[];
bridge_id?: string | null; bridge_id?: string | null;
completed_at: Date; completed_at: Date;
@ -138,7 +141,7 @@ export interface ISubIssuesState {
export interface ISubIssueResponse { export interface ISubIssueResponse {
state_distribution: ISubIssuesState; state_distribution: ISubIssuesState;
sub_issues: IIssue[]; sub_issues: TIssue[];
} }
export interface BlockeIssueDetail { export interface BlockeIssueDetail {
@ -240,13 +243,13 @@ export interface IIssueAttachment {
} }
export interface IIssueViewProps { export interface IIssueViewProps {
groupedIssues: { [key: string]: IIssue[] } | undefined; groupedIssues: { [key: string]: TIssue[] } | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined; displayFilters: IIssueDisplayFilterOptions | undefined;
isEmpty: boolean; isEmpty: boolean;
mutateIssues: KeyedMutator< mutateIssues: KeyedMutator<
| IIssue[] | TIssue[]
| { | {
[key: string]: IIssue[]; [key: string]: TIssue[];
} }
>; >;
params: any; params: any;
@ -254,3 +257,88 @@ export interface IIssueViewProps {
} }
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none"; export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export interface ViewFlags {
enableQuickAdd: boolean;
enableIssueCreation: boolean;
enableInlineEditing: boolean;
}
export type GroupByColumnTypes =
| "project"
| "state"
| "state_detail.group"
| "priority"
| "labels"
| "assignees"
| "created_by";
export interface IGroupByColumn {
id: string;
name: string;
Icon: ReactElement | undefined;
payload: Partial<TIssue>;
}
export interface IIssueMap {
[key: string]: TIssue;
}
// new issue structure types
export type TIssue = {
id: string;
name: string;
state_id: string;
description_html: string;
sort_order: number;
completed_at: string | null;
estimate_point: number | null;
priority: TIssuePriorities;
start_date: string | null;
target_date: string | null;
sequence_id: number;
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
label_ids: string[];
assignee_ids: string[];
sub_issues_count: number;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
attachment_count: number;
link_count: number;
is_subscribed: boolean;
archived_at: boolean;
is_draft: boolean;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
// issue details
related_issues: any;
issue_reactions: any;
issue_relations: any;
issue_cycle: any;
issue_module: any;
parent_detail: any;
issue_link: any;
};
export type TIssueMap = {
[issue_id: string]: TIssue;
};
export type TLoader = "init-loader" | "mutation" | undefined;
export type TGroupedIssues = {
[group_id: string]: string[];
};
export type TSubGroupedIssues = {
[sub_grouped_id: string]: {
[group_id: string]: string[];
};
};
export type TUnGroupedIssues = string[];

23
packages/types/src/issues/base.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
// issues
export * from "./issue";
export * from "./issue_reaction";
export * from "./issue_link";
export * from "./issue_attachment";
export * from "./issue_relation";
export * from "./issue_activity";
export * from "./issue_comment_reaction";
export * from "./issue_sub_issues";
export type TLoader = "init-loader" | "mutation" | undefined;
export type TGroupedIssues = {
[group_id: string]: string[];
};
export type TSubGroupedIssues = {
[sub_grouped_id: string]: {
[group_id: string]: string[];
};
};
export type TUnGroupedIssues = string[];

36
packages/types/src/issues/issue.d.ts vendored Normal file
View File

@ -0,0 +1,36 @@
// new issue structure types
export type TIssue = {
id: string;
name: string;
state_id: string;
description_html: string;
sort_order: number;
completed_at: string | null;
estimate_point: number | null;
priority: TIssuePriorities;
start_date: string;
target_date: string;
sequence_id: number;
project_id: string;
parent_id: string | null;
cycle_id: string | null;
module_id: string | null;
label_ids: string[];
assignee_ids: string[];
sub_issues_count: number;
created_at: string;
updated_at: string;
created_by: string;
updated_by: string;
attachment_count: number;
link_count: number;
is_subscribed: boolean;
archived_at: boolean;
is_draft: boolean;
// tempId is used for optimistic updates. It is not a part of the API response.
tempId?: string;
};
export type TIssueMap = {
[issue_id: string]: TIssue;
};

View File

@ -0,0 +1,41 @@
export type TIssueActivity = {
access?: "EXTERNAL" | "INTERNAL";
actor: string;
actor_detail: IUserLite;
attachments: any[];
comment?: string;
comment_html?: string;
comment_stripped?: string;
created_at: Date;
created_by: string;
field: string | null;
id: string;
issue: string | null;
issue_comment?: string | null;
issue_detail: {
description_html: string;
id: string;
name: string;
priority: string | null;
sequence_id: string;
} | null;
new_identifier: string | null;
new_value: string | null;
old_identifier: string | null;
old_value: string | null;
project: string;
project_detail: IProjectLite;
updated_at: Date;
updated_by: string;
verb: string;
workspace: string;
workspace_detail?: IWorkspaceLite;
};
export type TIssueActivityMap = {
[issue_id: string]: TIssueActivity;
};
export type TIssueActivityIdMap = {
[issue_id: string]: string[];
};

View File

@ -0,0 +1,23 @@
export type TIssueAttachment = {
id: string;
created_at: string;
updated_at: string;
attributes: {
name: string;
size: number;
};
asset: string;
created_by: string;
updated_by: string;
project: string;
workspace: string;
issue: string;
};
export type TIssueAttachmentMap = {
[issue_id: string]: TIssueAttachment;
};
export type TIssueAttachmentIdMap = {
[issue_id: string]: string[];
};

View File

@ -0,0 +1,20 @@
export type TIssueCommentReaction = {
id: string;
created_at: Date;
updated_at: Date;
reaction: string;
created_by: string;
updated_by: string;
project: string;
workspace: string;
actor: string;
comment: string;
};
export type TIssueCommentReactionMap = {
[issue_id: string]: TIssueCommentReaction;
};
export type TIssueCommentReactionIdMap = {
[issue_id: string]: string[];
};

View File

@ -0,0 +1,20 @@
export type TIssueLinkEditableFields = {
title: string;
url: string;
};
export type TIssueLink = TIssueLinkEditableFields & {
created_at: Date;
created_by: string;
created_by_detail: IUserLite;
id: string;
metadata: any;
};
export type TIssueLinkMap = {
[issue_id: string]: TIssueLink;
};
export type TIssueLinkIdMap = {
[issue_id: string]: string[];
};

View File

@ -0,0 +1,21 @@
export type TIssueReaction = {
actor: string;
actor_detail: IUserLite;
created_at: Date;
created_by: string;
id: string;
issue: string;
project: string;
reaction: string;
updated_at: Date;
updated_by: string;
workspace: string;
};
export type TIssueReactionMap = {
[issue_id: string]: TIssueReaction;
};
export type TIssueReactionIdMap = {
[issue_id: string]: string[];
};

View File

@ -0,0 +1,20 @@
import { TIssue } from "./issues";
export type TIssueRelationTypes =
| "blocking"
| "blocked_by"
| "duplicate"
| "relates_to";
export type TIssueRelationObject = { issue_detail: TIssue };
export type TIssueRelation = Record<
TIssueRelationTypes,
TIssueRelationObject[]
>;
export type TIssueRelationMap = {
[issue_id: string]: Record<TIssueRelationTypes, string[]>;
};
export type TIssueRelationIdMap = Record<TIssueRelationTypes, string[]>;

View File

@ -0,0 +1,22 @@
import { TIssue } from "./issue";
export type TSubIssuesStateDistribution = {
backlog: number;
unstarted: number;
started: number;
completed: number;
cancelled: number;
};
export type TIssueSubIssues = {
state_distribution: TSubIssuesStateDistribution;
sub_issues: TIssue[];
};
export type TIssueSubIssuesStateDistributionMap = {
[issue_id: string]: TSubIssuesStateDistribution;
};
export type TIssueSubIssuesIdMap = {
[issue_id: string]: string[];
};

View File

View File

@ -1,14 +1,14 @@
import type { import type {
IUser, IUser,
IUserLite, IUserLite,
IIssue, TIssue,
IProject, IProject,
IWorkspace, IWorkspace,
IWorkspaceLite, IWorkspaceLite,
IProjectLite, IProjectLite,
IIssueFilterOptions, IIssueFilterOptions,
ILinkDetails, ILinkDetails,
} from "types"; } from "@plane/types";
export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled"; export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
@ -58,7 +58,7 @@ export interface ModuleIssueResponse {
created_by: string; created_by: string;
id: string; id: string;
issue: string; issue: string;
issue_detail: IIssue; issue_detail: TIssue;
module: string; module: string;
module_detail: IModule; module_detail: IModule;
project: string; project: string;
@ -75,4 +75,4 @@ export type ModuleLink = {
export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined; export type SelectModuleType = (IModule & { actionType: "edit" | "delete" | "create-issue" }) | undefined;
export type SelectIssue = (IIssue & { actionType: "edit" | "delete" | "create" }) | undefined; export type SelectIssue = (TIssue & { actionType: "edit" | "delete" | "create" }) | undefined;

View File

@ -1,5 +1,5 @@
// types // types
import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types"; import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types";
export interface IPage { export interface IPage {
access: number; access: number;
@ -27,15 +27,11 @@ export interface IPage {
} }
export interface IRecentPages { export interface IRecentPages {
today: IPage[]; today: string[];
yesterday: IPage[]; yesterday: string[];
this_week: IPage[]; this_week: string[];
older: IPage[]; older: string[];
[key: string]: IPage[]; [key: string]: string[];
}
export interface RecentPagesResponse {
[key: string]: IPage[];
} }
export interface IPageBlock { export interface IPageBlock {
@ -47,7 +43,7 @@ export interface IPageBlock {
description_stripped: any; description_stripped: any;
id: string; id: string;
issue: string | null; issue: string | null;
issue_detail: IIssue | null; issue_detail: TIssue | null;
name: string; name: string;
page: string; page: string;
project: string; project: string;

View File

@ -1,6 +1,5 @@
import type { IUserLite, IWorkspace, IWorkspaceLite, IUserMemberLite, TStateGroups, IProjectViewProps } from "."; import { EUserProjectRoles } from "constants/project";
import type { IUser, IUserLite, IWorkspace, IWorkspaceLite, TStateGroups } from ".";
export type TUserProjectRole = 5 | 10 | 15 | 20;
export interface IProject { export interface IProject {
archive_in: number; archive_in: number;
@ -34,13 +33,10 @@ export interface IProject {
is_deployed: boolean; is_deployed: boolean;
is_favorite: boolean; is_favorite: boolean;
is_member: boolean; is_member: boolean;
member_role: TUserProjectRole | null; member_role: EUserProjectRoles | null;
members: IProjectMemberLite[]; members: IProjectMemberLite[];
issue_views_view: boolean;
module_view: boolean;
name: string; name: string;
network: number; network: number;
page_view: boolean;
project_lead: IUserLite | string | null; project_lead: IUserLite | string | null;
sort_order: number | null; sort_order: number | null;
total_cycles: number; total_cycles: number;
@ -64,6 +60,10 @@ type ProjectPreferences = {
}; };
}; };
export interface IProjectMap {
[id: string]: IProject;
}
export interface IProjectMemberLite { export interface IProjectMemberLite {
id: string; id: string;
member__avatar: string; member__avatar: string;
@ -77,7 +77,7 @@ export interface IProjectMember {
project: IProjectLite; project: IProjectLite;
workspace: IWorkspaceLite; workspace: IWorkspaceLite;
comment: string; comment: string;
role: TUserProjectRole; role: EUserProjectRoles;
preferences: ProjectPreferences; preferences: ProjectPreferences;
@ -90,27 +90,14 @@ export interface IProjectMember {
updated_by: string; updated_by: string;
} }
export interface IProjectMemberInvitation { export interface IProjectMembership {
id: string; id: string;
member: string;
project: IProject; role: EUserProjectRoles;
workspace: IWorkspace;
email: string;
accepted: boolean;
token: string;
message: string;
responded_at: Date;
role: TUserProjectRole;
created_at: Date;
updated_at: Date;
created_by: string;
updated_by: string;
} }
export interface IProjectBulkAddFormData { export interface IProjectBulkAddFormData {
members: { role: TUserProjectRole; member_id: string }[]; members: { role: EUserProjectRoles; member_id: string }[];
} }
export interface IGithubRepository { export interface IGithubRepository {

View File

@ -1,4 +1,4 @@
import { IProject, IProjectLite, IWorkspaceLite } from "types"; import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types";
export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "cancelled";

View File

@ -1,3 +1,4 @@
import { EUserProjectRoles } from "constants/project";
import { IIssueActivity, IIssueLite, TStateGroups } from "."; import { IIssueActivity, IIssueLite, TStateGroups } from ".";
export interface IUser { export interface IUser {
@ -61,11 +62,10 @@ export interface IUserTheme {
export interface IUserLite { export interface IUserLite {
avatar: string; avatar: string;
created_at: Date;
display_name: string; display_name: string;
email?: string; email?: string;
first_name: string; first_name: string;
readonly id: string; id: string;
is_bot: boolean; is_bot: boolean;
last_name: string; last_name: string;
} }
@ -163,7 +163,7 @@ export interface IUserProfileProjectSegregation {
} }
export interface IUserProjectsRole { export interface IUserProjectsRole {
[project_id: string]: number; [projectId: string]: EUserProjectRoles;
} }
// export interface ICurrentUser { // export interface ICurrentUser {

View File

@ -108,6 +108,18 @@ export interface IIssueDisplayProperties {
updated_on?: boolean; updated_on?: boolean;
} }
export interface IIssueFilters {
filters: IIssueFilterOptions | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined;
displayProperties: IIssueDisplayProperties | undefined;
}
export interface IIssueFiltersResponse {
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
}
export interface IWorkspaceIssueFilterOptions { export interface IWorkspaceIssueFilterOptions {
assignees?: string[] | null; assignees?: string[] | null;
created_by?: string[] | null; created_by?: string[] | null;

View File

@ -1,4 +1,4 @@
import { IIssueFilterOptions } from "./view-props"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props";
export interface IProjectView { export interface IProjectView {
id: string; id: string;
@ -10,6 +10,9 @@ export interface IProjectView {
updated_by: string; updated_by: string;
name: string; name: string;
description: string; description: string;
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
query: IIssueFilterOptions; query: IIssueFilterOptions;
query_data: IIssueFilterOptions; query_data: IIssueFilterOptions;
project: string; project: string;

View File

@ -1,4 +1,9 @@
import { IWorkspaceViewProps } from "./view-props"; import {
IWorkspaceViewProps,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
} from "./view-props";
export interface IWorkspaceView { export interface IWorkspaceView {
id: string; id: string;
@ -10,6 +15,9 @@ export interface IWorkspaceView {
updated_by: string; updated_by: string;
name: string; name: string;
description: string; description: string;
filters: IIssueIIFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
query: any; query: any;
query_data: IWorkspaceViewProps; query_data: IWorkspaceViewProps;
project: string; project: string;

View File

@ -1,6 +1,5 @@
import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "types"; import { EUserWorkspaceRoles } from "constants/workspace";
import type { IProjectMember, IUser, IUserLite, IWorkspaceViewProps } from "@plane/types";
export type TUserWorkspaceRole = 5 | 10 | 15 | 20;
export interface IWorkspace { export interface IWorkspace {
readonly id: string; readonly id: string;
@ -27,18 +26,23 @@ export interface IWorkspaceLite {
export interface IWorkspaceMemberInvitation { export interface IWorkspaceMemberInvitation {
accepted: boolean; accepted: boolean;
readonly id: string;
email: string; email: string;
token: string; id: string;
message: string; message: string;
responded_at: Date; responded_at: Date;
role: TUserWorkspaceRole; role: EUserWorkspaceRoles;
created_by_detail: IUser; token: string;
workspace: IWorkspace; workspace: string;
workspace_detail: {
id: string;
logo: string;
name: string;
slug: string;
};
} }
export interface IWorkspaceBulkInviteFormData { export interface IWorkspaceBulkInviteFormData {
emails: { email: string; role: TUserWorkspaceRole }[]; emails: { email: string; role: EUserWorkspaceRoles }[];
} }
export type Properties = { export type Properties = {
@ -58,15 +62,9 @@ export type Properties = {
}; };
export interface IWorkspaceMember { export interface IWorkspaceMember {
company_role: string | null;
created_at: Date;
created_by: string;
id: string; id: string;
member: IUserLite; member: IUserLite;
role: TUserWorkspaceRole; role: EUserWorkspaceRoles;
updated_at: Date;
updated_by: string;
workspace: IWorkspaceLite;
} }
export interface IWorkspaceMemberMe { export interface IWorkspaceMemberMe {
@ -76,7 +74,7 @@ export interface IWorkspaceMemberMe {
default_props: IWorkspaceViewProps; default_props: IWorkspaceViewProps;
id: string; id: string;
member: string; member: string;
role: TUserWorkspaceRole; role: EUserWorkspaceRoles;
updated_at: Date; updated_at: Date;
updated_by: string; updated_by: string;
view_props: IWorkspaceViewProps; view_props: IWorkspaceViewProps;

View File

@ -1,13 +1,16 @@
import * as React from "react"; import * as React from "react";
// icons
import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react"; import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react";
// types type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
import { IPriorityIcon } from "./type";
export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = "", transparentBg = false }) => { interface IPriorityIcon {
if (!className || className === "") className = "h-4 w-4"; className?: string;
priority: TIssuePriorities;
size?: number;
}
export const PriorityIcon: React.FC<IPriorityIcon> = (props) => {
const { priority, className = "", size = 14 } = props;
// Convert to lowercase for string comparison // Convert to lowercase for string comparison
const lowercasePriority = priority?.toLowerCase(); const lowercasePriority = priority?.toLowerCase();
@ -16,31 +19,17 @@ export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = ""
const getPriorityIcon = (): React.ReactNode => { const getPriorityIcon = (): React.ReactNode => {
switch (lowercasePriority) { switch (lowercasePriority) {
case "urgent": case "urgent":
return <AlertCircle className={`text-red-500 ${transparentBg ? "" : "p-0.5"} ${className}`} />; return <AlertCircle size={size} className={`text-red-500 ${className}`} />;
case "high": case "high":
return <SignalHigh className={`text-orange-500 ${transparentBg ? "" : "pl-1"} ${className}`} />; return <SignalHigh size={size} strokeWidth={3} className={`text-orange-500 ${className}`} />;
case "medium": case "medium":
return <SignalMedium className={`text-yellow-500 ${transparentBg ? "" : "ml-1.5"} ${className}`} />; return <SignalMedium size={size} strokeWidth={3} className={`text-yellow-500 ${className}`} />;
case "low": case "low":
return <SignalLow className={`text-green-500 ${transparentBg ? "" : "ml-2"} ${className}`} />; return <SignalLow size={size} strokeWidth={3} className={`text-custom-primary-100 ${className}`} />;
default: default:
return <Ban className={`text-custom-text-200 ${transparentBg ? "" : "p-0.5"} ${className}`} />; return <Ban size={size} className={`text-custom-text-200 ${className}`} />;
} }
}; };
return ( return <>{getPriorityIcon()}</>;
<>
{transparentBg ? (
getPriorityIcon()
) : (
<div
className={`grid h-5 w-5 place-items-center items-center rounded border ${
lowercasePriority === "urgent" ? "border-red-500/20 bg-red-500/20" : "border-custom-border-200"
}`}
>
{getPriorityIcon()}
</div>
)}
</>
);
}; };

View File

@ -1,11 +1,3 @@
export interface ISvgIcons extends React.SVGAttributes<SVGElement> { export interface ISvgIcons extends React.SVGAttributes<SVGElement> {
className?: string | undefined; className?: string | undefined;
} }
export type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export interface IPriorityIcon {
priority: TIssuePriorities | null;
className?: string;
transparentBg?: boolean | false;
}

View File

@ -4,8 +4,8 @@ import { useTheme } from "next-themes";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
import { mutate } from "swr"; import { mutate } from "swr";
// mobx store // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useUser } from "hooks/store";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// hooks // hooks
@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
// states // states
const [isDeactivating, setIsDeactivating] = useState(false); const [isDeactivating, setIsDeactivating] = useState(false);
const { const { deactivateAccount } = useUser();
user: { deactivateAccount },
} = useMobxStore();
const router = useRouter(); const router = useRouter();

View File

@ -10,7 +10,7 @@ import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IEmailCheckData } from "types/auth"; import { IEmailCheckData } from "@plane/types";
// constants // constants
import { ESignInSteps } from "components/account"; import { ESignInSteps } from "components/account";

View File

@ -1,9 +1,8 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services // services
import { AuthService } from "services/auth.service"; import { AuthService } from "services/auth.service";
// hooks // hooks
import { useApplication } from "hooks/store";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
import { GitHubSignInButton, GoogleSignInButton } from "components/account"; import { GitHubSignInButton, GoogleSignInButton } from "components/account";
@ -21,8 +20,8 @@ export const OAuthOptions: React.FC<Props> = observer((props) => {
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// mobx store // mobx store
const { const {
appConfig: { envConfig }, config: { envConfig },
} = useMobxStore(); } = useApplication();
const handleGoogleSignIn = async ({ clientId, credential }: any) => { const handleGoogleSignIn = async ({ clientId, credential }: any) => {
try { try {

View File

@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IPasswordSignInData } from "types/auth"; import { IPasswordSignInData } from "@plane/types";
// constants // constants
import { ESignInSteps } from "components/account"; import { ESignInSteps } from "components/account";

View File

@ -1,8 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks // hooks
import { useApplication } from "hooks/store";
import useSignInRedirection from "hooks/use-sign-in-redirection"; import useSignInRedirection from "hooks/use-sign-in-redirection";
// components // components
import { LatestFeatureBlock } from "components/common"; import { LatestFeatureBlock } from "components/common";
@ -38,8 +37,8 @@ export const SignInRoot = observer(() => {
const { handleRedirection } = useSignInRedirection(); const { handleRedirection } = useSignInRedirection();
// mobx store // mobx store
const { const {
appConfig: { envConfig }, config: { envConfig },
} = useMobxStore(); } = useApplication();
const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id); const isOAuthEnabled = envConfig && (envConfig.google_client_id || envConfig.github_client_id);

View File

@ -11,7 +11,7 @@ import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IPasswordSignInData } from "types/auth"; import { IPasswordSignInData } from "@plane/types";
type Props = { type Props = {
email: string; email: string;

View File

@ -9,7 +9,7 @@ import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IEmailCheckData } from "types/auth"; import { IEmailCheckData } from "@plane/types";
type Props = { type Props = {
email: string; email: string;

View File

@ -13,7 +13,7 @@ import { Button, Input } from "@plane/ui";
// helpers // helpers
import { checkEmailValidity } from "helpers/string.helper"; import { checkEmailValidity } from "helpers/string.helper";
// types // types
import { IEmailCheckData, IMagicSignInData } from "types/auth"; import { IEmailCheckData, IMagicSignInData } from "@plane/types";
// constants // constants
import { ESignInSteps } from "components/account"; import { ESignInSteps } from "components/account";
@ -233,8 +233,8 @@ export const UniqueCodeForm: React.FC<Props> = (props) => {
{resendTimerCode > 0 {resendTimerCode > 0
? `Request new code in ${resendTimerCode}s` ? `Request new code in ${resendTimerCode}s`
: isRequestingNewCode : isRequestingNewCode
? "Requesting new code" ? "Requesting new code"
: "Request new code"} : "Request new code"}
</button> </button>
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import { AnalyticsService } from "services/analytics.service";
// components // components
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics"; import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
// types // types
import { IAnalyticsParams } from "types"; import { IAnalyticsParams } from "@plane/types";
// fetch-keys // fetch-keys
import { ANALYTICS } from "constants/fetch-keys"; import { ANALYTICS } from "constants/fetch-keys";

View File

@ -3,7 +3,7 @@ import { BarTooltipProps } from "@nivo/bar";
import { DATE_KEYS } from "constants/analytics"; import { DATE_KEYS } from "constants/analytics";
import { renderMonthAndYear } from "helpers/analytics.helper"; import { renderMonthAndYear } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
type Props = { type Props = {
datum: BarTooltipProps<any>; datum: BarTooltipProps<any>;
@ -60,8 +60,8 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
? "capitalize" ? "capitalize"
: "" : ""
: params.x_axis === "priority" || params.x_axis === "state__group" : params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize" ? "capitalize"
: "" : ""
}`} }`}
> >
{params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}: {params.segment === "assignees__id" ? renderAssigneeName(tooltipValue.toString()) : tooltipValue}:

View File

@ -9,7 +9,7 @@ import { BarGraph } from "components/ui";
import { findStringWithMostCharacters } from "helpers/array.helper"; import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor, generateDisplayName } from "helpers/analytics.helper"; import { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
type Props = { type Props = {
analytics: IAnalyticsResponse; analytics: IAnalyticsResponse;
@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, param
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase() ? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
: "?" : "?"
: datum.value && datum.value !== "None" : datum.value && datum.value !== "None"
? `${datum.value}`.toUpperCase()[0] ? `${datum.value}`.toUpperCase()[0]
: "?"} : "?"}
</text> </text>
</g> </g>
</Tooltip> </Tooltip>

View File

@ -8,7 +8,7 @@ import { Button, Loader } from "@plane/ui";
// helpers // helpers
import { convertResponseToBarGraphData } from "helpers/analytics.helper"; import { convertResponseToBarGraphData } from "helpers/analytics.helper";
// types // types
import { IAnalyticsParams, IAnalyticsResponse } from "types"; import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
// fetch-keys // fetch-keys
import { ANALYTICS } from "constants/fetch-keys"; import { ANALYTICS } from "constants/fetch-keys";

View File

@ -1,13 +1,11 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Control, Controller, UseFormSetValue } from "react-hook-form"; import { Control, Controller, UseFormSetValue } from "react-hook-form";
// hooks
// mobx store import { useProject } from "hooks/store";
import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics"; import { SelectProject, SelectSegment, SelectXAxis, SelectYAxis } from "components/analytics";
// types // types
import { IAnalyticsParams } from "types"; import { IAnalyticsParams } from "@plane/types";
type Props = { type Props = {
control: Control<IAnalyticsParams, any>; control: Control<IAnalyticsParams, any>;
@ -20,12 +18,7 @@ type Props = {
export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => { export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
const { control, setValue, params, fullScreen, isProjectLevel } = props; const { control, setValue, params, fullScreen, isProjectLevel } = props;
const router = useRouter(); const { workspaceProjectIds: workspaceProjectIds } = useProject();
const { workspaceSlug } = router.query;
const { project: projectStore } = useMobxStore();
const projectsList = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : null;
return ( return (
<div <div
@ -40,7 +33,11 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
name="project" name="project"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<SelectProject value={value ?? undefined} onChange={onChange} projects={projectsList ?? undefined} /> <SelectProject
value={value ?? undefined}
onChange={onChange}
projectIds={workspaceProjectIds ?? undefined}
/>
)} )}
/> />
</div> </div>

View File

@ -1,25 +1,33 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// ui // ui
import { CustomSearchSelect } from "@plane/ui"; import { CustomSearchSelect } from "@plane/ui";
// types
import { IProject } from "types";
type Props = { type Props = {
value: string[] | undefined; value: string[] | undefined;
onChange: (val: string[] | null) => void; onChange: (val: string[] | null) => void;
projects: IProject[] | undefined; projectIds: string[] | undefined;
}; };
export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) => { export const SelectProject: React.FC<Props> = observer((props) => {
const options = projects?.map((project) => ({ const { value, onChange, projectIds } = props;
value: project.id, const { getProjectById } = useProject();
query: project.name + project.identifier,
content: ( const options = projectIds?.map((projectId) => {
<div className="flex items-center gap-2"> const projectDetails = getProjectById(projectId);
<span className="text-[0.65rem] text-custom-text-200">{project.identifier}</span>
{project.name} return {
</div> value: projectDetails?.id,
), query: `${projectDetails?.name} ${projectDetails?.identifier}`,
})); content: (
<div className="flex items-center gap-2">
<span className="text-[0.65rem] text-custom-text-200">{projectDetails?.identifier}</span>
{projectDetails?.name}
</div>
),
};
});
return ( return (
<CustomSearchSelect <CustomSearchSelect
@ -28,9 +36,9 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
options={options} options={options}
label={ label={
value && value.length > 0 value && value.length > 0
? projects ? projectIds
?.filter((p) => value.includes(p.id)) ?.filter((p) => value.includes(p))
.map((p) => p.identifier) .map((p) => getProjectById(p)?.name)
.join(", ") .join(", ")
: "All projects" : "All projects"
} }
@ -38,4 +46,4 @@ export const SelectProject: React.FC<Props> = ({ value, onChange, projects }) =>
multiple multiple
/> />
); );
}; });

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui // ui
import { CustomSelect } from "@plane/ui"; import { CustomSelect } from "@plane/ui";
// types // types
import { IAnalyticsParams, TXAxisValues } from "types"; import { IAnalyticsParams, TXAxisValues } from "@plane/types";
// constants // constants
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui // ui
import { CustomSelect } from "@plane/ui"; import { CustomSelect } from "@plane/ui";
// types // types
import { IAnalyticsParams, TXAxisValues } from "types"; import { IAnalyticsParams, TXAxisValues } from "@plane/types";
// constants // constants
import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics"; import { ANALYTICS_X_AXIS_VALUES } from "constants/analytics";

View File

@ -1,7 +1,7 @@
// ui // ui
import { CustomSelect } from "@plane/ui"; import { CustomSelect } from "@plane/ui";
// types // types
import { TYAxisValues } from "types"; import { TYAxisValues } from "@plane/types";
// constants // constants
import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics"; import { ANALYTICS_Y_AXIS_VALUES } from "constants/analytics";

View File

@ -1,65 +1,74 @@
import { observer } from "mobx-react-lite";
// hooks
import { useProject } from "hooks/store";
// icons // icons
import { Contrast, LayoutGrid, Users } from "lucide-react"; import { Contrast, LayoutGrid, Users } from "lucide-react";
// helpers // helpers
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { truncateText } from "helpers/string.helper"; import { truncateText } from "helpers/string.helper";
// types
import { IProject } from "types";
type Props = { type Props = {
projects: IProject[]; projectIds: string[];
}; };
export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = (props) => { export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((props) => {
const { projects } = props; const { projectIds } = props;
const { getProjectById } = useProject();
return ( return (
<div className="hidden h-full overflow-hidden md:flex md:flex-col"> <div className="hidden h-full overflow-hidden md:flex md:flex-col">
<h4 className="font-medium">Selected Projects</h4> <h4 className="font-medium">Selected Projects</h4>
<div className="mt-4 h-full space-y-6 overflow-y-auto"> <div className="mt-4 h-full space-y-6 overflow-y-auto">
{projects.map((project) => ( {projectIds.map((projectId) => {
<div key={project.id} className="w-full"> const project = getProjectById(projectId);
<div className="flex items-center gap-1 text-sm">
{project.emoji ? ( if (!project) return;
<span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
) : project.icon_prop ? ( return (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div> <div key={projectId} className="w-full">
) : ( <div className="flex items-center gap-1 text-sm">
<span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white"> {project.emoji ? (
{project?.name.charAt(0)} <span className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.emoji)}</span>
</span> ) : project.icon_prop ? (
)} <div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(project.icon_prop)}</div>
<h5 className="flex items-center gap-1"> ) : (
<p className="break-words">{truncateText(project.name, 20)}</p> <span className="mr-1 grid h-6 w-6 flex-shrink-0 place-items-center rounded bg-gray-700 uppercase text-white">
<span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span> {project?.name.charAt(0)}
</h5> </span>
</div> )}
<div className="mt-4 w-full space-y-3 pl-2"> <h5 className="flex items-center gap-1">
<div className="flex items-center justify-between gap-2 text-xs"> <p className="break-words">{truncateText(project.name, 20)}</p>
<div className="flex items-center gap-2"> <span className="ml-1 text-xs text-custom-text-200">({project.identifier})</span>
<Users className="text-custom-text-200" size={14} strokeWidth={2} /> </h5>
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
</div> </div>
<div className="flex items-center justify-between gap-2 text-xs"> <div className="mt-4 w-full space-y-3 pl-2">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between gap-2 text-xs">
<Contrast className="text-custom-text-200" size={14} strokeWidth={2} /> <div className="flex items-center gap-2">
<h6>Total cycles</h6> <Users className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total members</h6>
</div>
<span className="text-custom-text-200">{project.total_members}</span>
</div> </div>
<span className="text-custom-text-200">{project.total_cycles}</span> <div className="flex items-center justify-between gap-2 text-xs">
</div> <div className="flex items-center gap-2">
<div className="flex items-center justify-between gap-2 text-xs"> <Contrast className="text-custom-text-200" size={14} strokeWidth={2} />
<div className="flex items-center gap-2"> <h6>Total cycles</h6>
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} /> </div>
<h6>Total modules</h6> <span className="text-custom-text-200">{project.total_cycles}</span>
</div>
<div className="flex items-center justify-between gap-2 text-xs">
<div className="flex items-center gap-2">
<LayoutGrid className="text-custom-text-200" size={14} strokeWidth={2} />
<h6>Total modules</h6>
</div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div> </div>
<span className="text-custom-text-200">{project.total_modules}</span>
</div> </div>
</div> </div>
</div> );
))} })}
</div> </div>
</div> </div>
); );
}; });

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