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 9d2e0e29e7, reversing
changes made to 9595493c42.

* 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 1cc18a0915.

* 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:
pull_request:
branches:
- preview
- develop # Change this to preview
types:
- closed
env:
@ -33,14 +33,23 @@ jobs:
sudo apt update
sudo apt install gh -y
- name: Push Changes to Target Repo
- name: Create Pull Request
env:
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
run: |
TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}"
TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}"
TARGET_BASE_BRANCH="${{ secrets.SYNC_TARGET_BASE_BRANCH_NAME }}"
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
git checkout $SOURCE_BRANCH
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](
getattr(instance, expand)
)
response[expand] = exp_serializer.data
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)

View File

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

View File

@ -9,11 +9,12 @@ class DynamicBaseSerializer(BaseSerializer):
def __init__(self, *args, **kwargs):
# 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.
fields = kwargs.pop("fields", None)
fields = kwargs.pop("fields", [])
self.expand = kwargs.pop("expand", []) or []
fields = self.expand
# Call the initialization of the superclass.
super().__init__(*args, **kwargs)
# If 'fields' was provided, filter the fields of the serializer accordingly.
if fields is not None:
self.fields = self._filter_fields(fields)
@ -47,12 +48,91 @@ class DynamicBaseSerializer(BaseSerializer):
elif isinstance(item, dict):
allowed.append(list(item.keys())[0])
# Convert the current serializer's fields and the allowed fields to sets.
existing = set(self.fields)
allowed = set(allowed)
for field in allowed:
if field not in self.fields:
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueFlatSerializer,
)
# Remove fields from the serializer that aren't in the 'allowed' list.
for field_name in (existing - allowed):
self.fields.pop(field_name)
# 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,
"parent": IssueFlatSerializer,
}
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
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 .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import Cycle, CycleIssue, CycleFavorite
from plane.db.models import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
class CycleWriteSerializer(BaseSerializer):
@ -106,3 +106,15 @@ class CycleFavoriteSerializer(BaseSerializer):
"project",
"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)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
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):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
issue_detail = IssueRelationLiteSerializer(read_only=True, source="related_issue")
class Meta:
model = IssueRelation
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
@ -296,16 +307,12 @@ class IssueRelationSerializer(BaseSerializer):
]
class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
issue_detail = IssueRelationLiteSerializer(read_only=True, source="issue")
class Meta:
model = IssueRelation
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
@ -512,7 +519,6 @@ class IssueStateSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
bridge_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
@ -521,32 +527,58 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__"
class IssueSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateSerializer(read_only=True, source="state")
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
class IssueSerializer(DynamicBaseSerializer):
# ids
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_id = serializers.PrimaryKeyRelatedField(read_only=True)
# Many to many
label_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="labels")
assignee_ids = serializers.PrimaryKeyRelatedField(read_only=True, many=True, source="assignees")
# Count items
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:
model = Issue
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"created_by",
"updated_by",
fields = [
"id",
"name",
"state_id",
"description_html",
"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",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_subscribed",
"is_draft",
"archived_at",
]
read_only_fields = fields
class IssueLiteSerializer(DynamicBaseSerializer):

View File

@ -2,7 +2,7 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer
from .project import ProjectLiteSerializer
from .workspace import WorkspaceLiteSerializer
@ -14,6 +14,7 @@ from plane.db.models import (
ModuleIssue,
ModuleLink,
ModuleFavorite,
ModuleUserProperties,
)
@ -159,7 +160,7 @@ class ModuleLinkSerializer(BaseSerializer):
return ModuleLink.objects.create(**validated_data)
class ModuleSerializer(BaseSerializer):
class ModuleSerializer(DynamicBaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
lead_detail = UserLiteSerializer(read_only=True, source="lead")
members_detail = UserLiteSerializer(read_only=True, many=True, source="members")
@ -196,3 +197,14 @@ class ModuleFavoriteSerializer(BaseSerializer):
"project",
"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
fields = "__all__"
class ProjectMemberRoleSerializer(DynamicBaseSerializer):
class Meta:
model = ProjectMember
fields = ("id", "role", "member", "project")
class ProjectMemberInviteSerializer(BaseSerializer):
project = ProjectLiteSerializer(read_only=True)

View File

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

View File

@ -2,7 +2,7 @@
from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
@ -13,10 +13,11 @@ from plane.db.models import (
TeamMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
)
class WorkSpaceSerializer(BaseSerializer):
class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True)
total_members = 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)
workspace = WorkspaceLiteSerializer(read_only=True)
@ -78,7 +79,7 @@ class WorkspaceMemberMeSerializer(BaseSerializer):
fields = "__all__"
class WorkspaceMemberAdminSerializer(BaseSerializer):
class WorkspaceMemberAdminSerializer(DynamicBaseSerializer):
member = UserAdminLiteSerializer(read_only=True)
workspace = WorkspaceLiteSerializer(read_only=True)
@ -161,3 +162,13 @@ class WorkspaceThemeSerializer(BaseSerializer):
"workspace",
"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,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
)
@ -44,7 +45,7 @@ urlpatterns = [
name="project-issue-cycle",
),
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(
{
"get": "retrieve",
@ -84,4 +85,9 @@ urlpatterns = [
TransferCycleIssueEndpoint.as_view(),
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",
),
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(
{
"get": "retrieve",

View File

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

View File

@ -7,6 +7,7 @@ from plane.app.views import (
ModuleLinkViewSet,
ModuleFavoriteViewSet,
BulkImportModulesEndpoint,
ModuleUserPropertiesEndpoint
)
@ -44,7 +45,7 @@ urlpatterns = [
name="project-module-issues",
),
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(
{
"get": "retrieve",
@ -101,4 +102,9 @@ urlpatterns = [
BulkImportModulesEndpoint.as_view(),
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,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewFavoriteViewSet,
IssueViewFavoriteViewSet,
)

View File

@ -18,6 +18,8 @@ from plane.app.views import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
)
@ -92,6 +94,11 @@ urlpatterns = [
WorkSpaceMemberViewSet.as_view({"get": "list"}),
name="workspace-member",
),
path(
"workspaces/<str:slug>/project-members/",
WorkspaceProjectMemberEndpoint.as_view(),
name="workspace-member-roles",
),
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
@ -195,4 +202,9 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(),
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,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
)
from .state import StateViewSet
from .view import (
@ -59,6 +61,7 @@ from .cycle import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleUserPropertiesEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .issue import (
@ -103,6 +106,7 @@ from .module import (
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleUserPropertiesEndpoint,
)
from .api import ApiTokenEndpoint

View File

@ -159,6 +159,21 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
if resolve(self.request.path_info).url_name == "project":
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):
permission_classes = [
@ -239,3 +254,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def project_id(self):
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,
When,
Value,
CharField
CharField,
)
from django.core import serializers
from django.utils import timezone
@ -33,8 +33,9 @@ from plane.app.serializers import (
CycleFavoriteSerializer,
IssueStateSerializer,
CycleWriteSerializer,
CycleUserPropertiesSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import (
User,
Cycle,
@ -44,6 +45,7 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
Label,
CycleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@ -164,23 +166,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
.annotate(
status=Case(
When(
Q(start_date__lte=timezone.now()) & 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")
Q(start_date__lte=timezone.now())
& 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(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
then=Value("DRAFT")
then=Value("DRAFT"),
),
default=Value("DRAFT"),
output_field=CharField(),
default=Value("DRAFT"),
output_field=CharField(),
)
)
.prefetch_related(
@ -202,6 +199,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
def list(self, request, slug, project_id):
queryset = self.get_queryset()
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")
@ -307,44 +305,8 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
return Response(data, status=status.HTTP_200_OK)
# Upcoming Cycles
if cycle_view == "upcoming":
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
)
cycles = CycleSerializer(queryset, many=True).data
return Response(cycles, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
if (
@ -576,7 +538,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
@ -600,12 +561,10 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
)
issues = IssueStateSerializer(
serializer = IssueStateSerializer(
issues, many=True, fields=fields if fields else None
).data
issue_dict = {str(issue["id"]): issue for issue in issues}
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, cycle_id):
issues = request.data.get("issues", [])
@ -698,11 +657,13 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
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(
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(
type="cycle.activity.deleted",
requested_data=json.dumps(
@ -712,7 +673,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
}
),
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)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
@ -834,3 +795,39 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
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,
)
.filter(**filters)
.annotate(bridge_id=F("issue_inbox__id"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
@ -204,9 +203,9 @@ class InboxIssueViewSet(BaseViewSet):
serializer = IssueStateInboxSerializer(issue)
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(
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
project_member = ProjectMember.objects.get(
@ -316,19 +315,16 @@ class InboxIssueViewSet(BaseViewSet):
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
)
def retrieve(self, request, slug, project_id, inbox_id, pk):
inbox_issue = InboxIssue.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
)
def retrieve(self, request, slug, project_id, inbox_id, issue_id):
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)
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(
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
project_member = ProjectMember.objects.get(
@ -350,7 +346,7 @@ class InboxIssueViewSet(BaseViewSet):
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
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()
inbox_issue.delete()

View File

@ -52,6 +52,7 @@ from plane.app.serializers import (
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
IssueRelationLiteSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
@ -129,22 +130,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
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(module_id=F("issue_module__module_id"))
.annotate(
@ -159,7 +144,26 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="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
if order_by_param == "priority" or order_by_param == "-priority":
@ -217,9 +221,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueSerializer(
issue_queryset, many=True, fields=self.fields, expand=self.expand
).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
@ -256,7 +261,10 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).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):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
@ -590,16 +598,19 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
ProjectLitePermission,
]
def post(self, request, slug, project_id):
issue_property, created = IssueProperty.objects.get_or_create(
def patch(self, request, slug, project_id):
issue_property = IssueProperty.objects.get(
user=request.user,
project_id=project_id,
)
if not created:
issue_property.properties = request.data.get("properties", {})
issue_property.save()
issue_property.properties = request.data.get("properties", {})
issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.display_filters = request.data.get(
"display_filters", issue_property.display_filters
)
issue_property.display_properties = request.data.get(
"display_properties", issue_property.display_properties
)
issue_property.save()
serializer = IssuePropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -708,6 +719,13 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
is_subscribed=Exists(
IssueSubscriber.objects.filter(
subscriber=self.request.user, issue_id=OuterRef("id")
)
)
)
.prefetch_related(
Prefetch(
"issue_reactions",
@ -728,7 +746,7 @@ class SubIssuesEndpoint(BaseAPIView):
item["state_group"]: item["state_count"] for item in state_distribution
}
serializer = IssueLiteSerializer(
serializer = IssueSerializer(
sub_issues,
many=True,
)
@ -775,7 +793,7 @@ class SubIssuesEndpoint(BaseAPIView):
]
return Response(
IssueFlatSerializer(updated_sub_issues, many=True).data,
IssueSerializer(updated_sub_issues, many=True).data,
status=status.HTTP_200_OK,
)
@ -1062,9 +1080,10 @@ class IssueArchiveViewSet(BaseViewSet):
else issue_queryset.filter(parent__isnull=True)
)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(
@ -1365,23 +1384,62 @@ class IssueRelationViewSet(BaseViewSet):
.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):
related_list = request.data.get("related_list", [])
relation = request.data.get("relation", None)
relation_type = request.data.get("relation_type", None)
issues = request.data.get("issues", [])
project = Project.objects.get(pk=project_id)
issue_relation = IssueRelation.objects.bulk_create(
[
IssueRelation(
issue_id=related_issue["issue"],
related_issue_id=related_issue["related_issue"],
relation_type=related_issue["relation_type"],
issue_id=issue if relation_type == "blocking" else issue_id,
related_issue_id=issue_id if relation_type == "blocking" else issue,
relation_type="blocked_by" if relation_type == "blocking" else relation_type,
project_id=project_id,
workspace_id=project.workspace_id,
created_by=request.user,
updated_by=request.user,
)
for related_issue in related_list
for issue in issues
],
batch_size=10,
ignore_conflicts=True,
@ -1397,7 +1455,7 @@ class IssueRelationViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
)
if relation == "blocking":
if relation_type == "blocking":
return Response(
RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED,
@ -1408,10 +1466,18 @@ class IssueRelationViewSet(BaseViewSet):
status=status.HTTP_201_CREATED,
)
def destroy(self, request, slug, project_id, issue_id, pk):
issue_relation = IssueRelation.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
def remove_relation(self, request, slug, project_id, issue_id):
relation_type = request.data.get("relation_type", None)
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(
IssueRelationSerializer(issue_relation).data,
cls=DjangoJSONEncoder,
@ -1419,7 +1485,7 @@ class IssueRelationViewSet(BaseViewSet):
issue_relation.delete()
issue_activity.delay(
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),
issue_id=str(issue_id),
project_id=str(project_id),
@ -1547,9 +1613,10 @@ class IssueDraftViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
project = Project.objects.get(pk=project_id)
@ -1626,4 +1693,4 @@ class IssueDraftViewSet(BaseViewSet):
current_instance=current_instance,
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,
ModuleFavoriteSerializer,
IssueStateSerializer,
ModuleUserPropertiesSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import ProjectEntityPermission, ProjectLitePermission
from plane.db.models import (
Module,
ModuleIssue,
@ -32,6 +33,7 @@ from plane.db.models import (
ModuleFavorite,
IssueLink,
IssueAttachment,
ModuleUserProperties,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results
@ -54,7 +56,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
)
def get_queryset(self):
subquery = ModuleFavorite.objects.filter(
user=self.request.user,
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):
@ -153,6 +154,14 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
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):
queryset = self.get_queryset().get(pk=pk)
@ -289,7 +298,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
webhook_event = "module_issue"
bulk = True
filterset_fields = [
"issue__labels__id",
"issue__assignees__id",
@ -335,7 +343,6 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_module__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
@ -359,9 +366,10 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.values("count")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
serializer = IssueStateSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, module_id):
issues = request.data.get("issues", [])
@ -444,20 +452,23 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
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(
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(
type="module.activity.deleted",
requested_data=json.dumps(
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
"issues": [str(issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(module_issue.issue_id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
@ -521,4 +532,42 @@ class ModuleFavoriteViewSet(BaseViewSet):
module_id=module_id,
)
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):
queryset = self.get_queryset().filter(archived_at__isnull=True)
return Response(
PageSerializer(queryset, many=True).data, status=status.HTTP_200_OK
)
pages = PageSerializer(queryset, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def archive(self, request, slug, project_id, page_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,
).filter(archived_at__isnull=False)
return Response(
PageSerializer(pages, many=True).data, status=status.HTTP_200_OK
)
pages = PageSerializer(pages, many=True).data
return Response(pages, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, pk):
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,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectMemberRoleSerializer,
)
from plane.app.permissions import (
@ -180,12 +181,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
projects, many=True
).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):
try:
@ -713,13 +711,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, slug, project_id):
project_member = ProjectMember.objects.get(
member=request.user,
workspace__slug=slug,
project_id=project_id,
is_active=True,
)
# Get the list of project members for the project
project_members = ProjectMember.objects.filter(
project_id=project_id,
workspace__slug=slug,
@ -727,10 +719,7 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True,
).select_related("project", "member", "workspace")
if project_member.role > 10:
serializer = ProjectMemberAdminSerializer(project_members, many=True)
else:
serializer = ProjectMemberSerializer(project_members, many=True)
serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, pk):
@ -1010,18 +999,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request):
files = []
s3_client_params = {
"service_name": "s3",
"aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
"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)
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
@ -1034,19 +1016,9 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
if not content["Key"].endswith(
"/"
): # This line ensures we're only getting files, not "sub-folders"
if (
hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
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']}"
)
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)

View File

@ -27,7 +27,12 @@ from plane.app.serializers import (
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
)
from plane.db.models import (
Workspace,
GlobalView,
@ -43,8 +48,8 @@ from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet):
serializer_class = GlobalViewSerializer
model = GlobalView
serializer_class = IssueViewSerializer
model = IssueView
permission_classes = [
WorkspaceEntityPermission,
]
@ -58,6 +63,7 @@ class GlobalViewViewSet(BaseViewSet):
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__isnull=True)
.select_related("workspace")
.order_by(self.request.GET.get("order_by", "-created_at"))
.distinct()
@ -179,12 +185,10 @@ class GlobalViewIssuesViewSet(BaseViewSet):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(
issue_dict,
status=status.HTTP_200_OK,
serializer = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet):
@ -217,6 +221,14 @@ class IssueViewViewSet(BaseViewSet):
.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):
serializer_class = IssueViewFavoriteSerializer
@ -246,4 +258,4 @@ class IssueViewFavoriteViewSet(BaseViewSet):
view_id=view_id,
)
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,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer,
)
from plane.app.views.base import BaseAPIView
from . import BaseViewSet
@ -64,6 +66,7 @@ from plane.db.models import (
WorkspaceMember,
CycleIssue,
IssueReaction,
WorkspaceUserProperties
)
from plane.app.permissions import (
WorkSpaceBasePermission,
@ -71,11 +74,13 @@ from plane.app.permissions import (
WorkspaceEntityPermission,
WorkspaceViewerPermission,
WorkspaceUserPermission,
ProjectLitePermission,
)
from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.event_tracking_task import workspace_invite_event
class WorkSpaceViewSet(BaseViewSet):
model = Workspace
serializer_class = WorkSpaceSerializer
@ -173,6 +178,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
]
def get(self, request):
fields = [field for field in request.GET.get("fields", "").split(",") if field]
member_count = (
WorkspaceMember.objects.filter(
workspace=OuterRef("id"),
@ -208,9 +214,12 @@ class UserWorkSpacesEndpoint(BaseAPIView):
)
.distinct()
)
serializer = WorkSpaceSerializer(self.filter_queryset(workspace), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
workspaces = WorkSpaceSerializer(
self.filter_queryset(workspace),
fields=fields if fields else None,
many=True,
).data
return Response(workspaces, status=status.HTTP_200_OK)
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
@ -407,7 +416,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
# Delete the invitation
workspace_invite.delete()
# Send event
workspace_invite_event.delay(
user=user.id if user is not None else None,
@ -537,10 +546,15 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_members = self.get_queryset()
if workspace_member.role > 10:
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
serializer = WorkspaceMemberAdminSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
else:
serializer = WorkSpaceMemberSerializer(
workspace_members,
fields=("id", "member", "role"),
many=True,
)
return Response(serializer.data, status=status.HTTP_200_OK)
@ -705,6 +719,43 @@ class WorkSpaceMemberViewSet(BaseViewSet):
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):
serializer_class = TeamSerializer
model = Team
@ -1334,8 +1385,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
issues = IssueLiteSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(issue_dict, status=status.HTTP_200_OK)
return Response(issues, status=status.HTTP_200_OK)
class WorkspaceLabelsEndpoint(BaseAPIView):
@ -1349,3 +1399,30 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
project__project_projectmember__member=request.user,
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
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,
):
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
new_parent = Issue.objects.filter(pk=requested_data.get("parent")).first() if requested_data.get("parent") is not None else None
old_parent = (
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(
IssueActivity(
@ -714,7 +722,9 @@ def create_cycle_issue_activity(
cycle = Cycle.objects.filter(
pk=created_record.get("fields").get("cycle")
).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:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
@ -830,7 +840,9 @@ def create_module_issue_activity(
module = Module.objects.filter(
pk=created_record.get("fields").get("module")
).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:
issue.updated_at = timezone.now()
issue.save(update_fields=["updated_at"])
@ -1276,40 +1288,42 @@ def create_issue_relation_activity(
current_instance = (
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:
for issue_relation in requested_data.get("related_list"):
if issue_relation.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
if current_instance is None and requested_data.get("issues") is not None:
for related_issue in requested_data.get("issues"):
issue = Issue.objects.get(pk=related_issue)
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("related_issue"),
issue_id=issue_id,
actor_id=actor_id,
verb="created",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field=relation_type,
field=requested_data.get("relation_type"),
project_id=project_id,
workspace_id=workspace_id,
comment=f"added {relation_type} relation",
old_identifier=issue_relation.get("issue"),
comment=f"added {requested_data.get('relation_type')} relation",
old_identifier=related_issue,
)
)
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
issue = Issue.objects.get(pk=issue_id)
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("issue"),
issue_id=related_issue,
actor_id=actor_id,
verb="created",
old_value="",
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,
workspace_id=workspace_id,
comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("related_issue"),
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_id,
epoch=epoch,
)
)
@ -1329,44 +1343,44 @@ def delete_issue_relation_activity(
current_instance = (
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:
if current_instance.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = current_instance.get("relation_type")
issue = Issue.objects.get(pk=current_instance.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("related_issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=relation_type,
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=requested_data.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=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,
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field=f'{current_instance.get("relation_type")}',
project_id=project_id,
workspace_id=workspace_id,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
epoch=epoch,
)
)
issue = Issue.objects.get(pk=issue_id)
issue_activities.append(
IssueActivity(
issue_id=requested_data.get("related_issue"),
actor_id=actor_id,
verb="deleted",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
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,
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(
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"
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")
if issue_comment is not None:
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")
.first()
)
actor = User.objects.get(pk=actor_id)
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,
TeamMember,
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceBaseModel,
)
from .project import (
@ -48,11 +50,11 @@ from .social_connection import SocialLoginConnection
from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite
from .cycle import Cycle, CycleIssue, CycleFavorite, CycleUserProperties
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

View File

@ -6,6 +6,47 @@ from django.conf import settings
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):
name = models.CharField(max_length=255, verbose_name="Cycle Name")
description = models.TextField(verbose_name="Cycle Description", blank=True)
@ -89,3 +130,28 @@ class CycleFavorite(ProjectBaseModel):
def __str__(self):
"""Return user and the cycle"""
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
class IssueManager(models.Manager):
def get_queryset(self):
@ -394,7 +436,9 @@ class IssueProperty(ProjectBaseModel):
on_delete=models.CASCADE,
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:
verbose_name = "Issue Property"

View File

@ -6,6 +6,47 @@ from django.conf import settings
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):
name = models.CharField(max_length=255, verbose_name="Module Name")
description = models.TextField(verbose_name="Module Description", blank=True)
@ -141,3 +182,28 @@ class ModuleFavorite(ProjectBaseModel):
def __str__(self):
"""Return user and the module"""
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
# 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):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
@ -40,14 +81,17 @@ class GlobalView(BaseModel):
return f"{self.name} <{self.workspace.name}>"
class IssueView(ProjectBaseModel):
class IssueView(WorkspaceBaseModel):
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
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(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
class Meta:
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():
return {
@ -103,6 +148,22 @@ class Workspace(BaseModel):
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):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_member"
@ -218,3 +279,28 @@ class WorkspaceTheme(BaseModel):
verbose_name_plural = "Workspace Themes"
db_table = "workspace_themes"
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
dj-database-url==2.1.0
posthog==3.0.2
cryptography==41.0.6
cryptography==41.0.5
lxml==4.9.3
boto3==1.28.40

View File

@ -39,7 +39,7 @@ function download(){
echo ""
echo "Latest version is now available for you to use"
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 ""
}

View File

@ -10,7 +10,8 @@
"packages/eslint-config-custom",
"packages/tailwind-config-custom",
"packages/tsconfig",
"packages/ui"
"packages/ui",
"packages/types"
],
"scripts": {
"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 {
response: string;

View File

@ -1,6 +1,4 @@
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
export interface IAppConfig {
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";
@ -68,7 +68,7 @@ export type TLabelsDistribution = {
export interface CycleIssueResponse {
id: string;
issue_detail: IIssue;
issue_detail: TIssue;
created_at: Date;
updated_at: Date;
created_by: string;
@ -82,7 +82,7 @@ export interface CycleIssueResponse {
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 = {
start_date: string;

View File

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

View File

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

View File

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

View File

@ -21,6 +21,11 @@ export * from "./reaction";
export * from "./view-props";
export * from "./workspace-views";
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> = {
[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 type {
IState,
@ -10,7 +11,8 @@ import type {
IStateLite,
Properties,
IIssueDisplayFilterOptions,
} from "types";
IIssueReaction,
} from "@plane/types";
export interface IIssueCycle {
id: string;
@ -83,6 +85,7 @@ export interface IIssue {
attachment_count: number;
attachments: any[];
issue_relations: IssueRelation[];
issue_reactions: IIssueReaction[];
related_issues: IssueRelation[];
bridge_id?: string | null;
completed_at: Date;
@ -138,7 +141,7 @@ export interface ISubIssuesState {
export interface ISubIssueResponse {
state_distribution: ISubIssuesState;
sub_issues: IIssue[];
sub_issues: TIssue[];
}
export interface BlockeIssueDetail {
@ -240,13 +243,13 @@ export interface IIssueAttachment {
}
export interface IIssueViewProps {
groupedIssues: { [key: string]: IIssue[] } | undefined;
groupedIssues: { [key: string]: TIssue[] } | undefined;
displayFilters: IIssueDisplayFilterOptions | undefined;
isEmpty: boolean;
mutateIssues: KeyedMutator<
| IIssue[]
| TIssue[]
| {
[key: string]: IIssue[];
[key: string]: TIssue[];
}
>;
params: any;
@ -254,3 +257,88 @@ export interface IIssueViewProps {
}
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 {
IUser,
IUserLite,
IIssue,
TIssue,
IProject,
IWorkspace,
IWorkspaceLite,
IProjectLite,
IIssueFilterOptions,
ILinkDetails,
} from "types";
} from "@plane/types";
export type TModuleStatus = "backlog" | "planned" | "in-progress" | "paused" | "completed" | "cancelled";
@ -58,7 +58,7 @@ export interface ModuleIssueResponse {
created_by: string;
id: string;
issue: string;
issue_detail: IIssue;
issue_detail: TIssue;
module: string;
module_detail: IModule;
project: string;
@ -75,4 +75,4 @@ export type ModuleLink = {
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
import { IIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "types";
import { TIssue, IIssueLabel, IWorkspaceLite, IProjectLite } from "@plane/types";
export interface IPage {
access: number;
@ -27,15 +27,11 @@ export interface IPage {
}
export interface IRecentPages {
today: IPage[];
yesterday: IPage[];
this_week: IPage[];
older: IPage[];
[key: string]: IPage[];
}
export interface RecentPagesResponse {
[key: string]: IPage[];
today: string[];
yesterday: string[];
this_week: string[];
older: string[];
[key: string]: string[];
}
export interface IPageBlock {
@ -47,7 +43,7 @@ export interface IPageBlock {
description_stripped: any;
id: string;
issue: string | null;
issue_detail: IIssue | null;
issue_detail: TIssue | null;
name: string;
page: string;
project: string;

View File

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

View File

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

View File

@ -108,6 +108,18 @@ export interface IIssueDisplayProperties {
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 {
assignees?: 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 {
id: string;
@ -10,6 +10,9 @@ export interface IProjectView {
updated_by: string;
name: string;
description: string;
filters: IIssueFilterOptions;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
query: IIssueFilterOptions;
query_data: IIssueFilterOptions;
project: string;

View File

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

View File

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

View File

@ -1,13 +1,16 @@
import * as React from "react";
// icons
import { AlertCircle, Ban, SignalHigh, SignalLow, SignalMedium } from "lucide-react";
// types
import { IPriorityIcon } from "./type";
type TIssuePriorities = "urgent" | "high" | "medium" | "low" | "none";
export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = "", transparentBg = false }) => {
if (!className || className === "") className = "h-4 w-4";
interface IPriorityIcon {
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
const lowercasePriority = priority?.toLowerCase();
@ -16,31 +19,17 @@ export const PriorityIcon: React.FC<IPriorityIcon> = ({ priority, className = ""
const getPriorityIcon = (): React.ReactNode => {
switch (lowercasePriority) {
case "urgent":
return <AlertCircle className={`text-red-500 ${transparentBg ? "" : "p-0.5"} ${className}`} />;
return <AlertCircle size={size} className={`text-red-500 ${className}`} />;
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":
return <SignalMedium className={`text-yellow-500 ${transparentBg ? "" : "ml-1.5"} ${className}`} />;
return <SignalMedium size={size} strokeWidth={3} className={`text-yellow-500 ${className}`} />;
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:
return <Ban className={`text-custom-text-200 ${transparentBg ? "" : "p-0.5"} ${className}`} />;
return <Ban size={size} className={`text-custom-text-200 ${className}`} />;
}
};
return (
<>
{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>
)}
</>
);
return <>{getPriorityIcon()}</>;
};

View File

@ -1,11 +1,3 @@
export interface ISvgIcons extends React.SVGAttributes<SVGElement> {
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 { Trash2 } from "lucide-react";
import { mutate } from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { useUser } from "hooks/store";
// ui
import { Button } from "@plane/ui";
// hooks
@ -22,9 +22,7 @@ export const DeactivateAccountModal: React.FC<Props> = (props) => {
// states
const [isDeactivating, setIsDeactivating] = useState(false);
const {
user: { deactivateAccount },
} = useMobxStore();
const { deactivateAccount } = useUser();
const router = useRouter();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { AnalyticsService } from "services/analytics.service";
// components
import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSidebar } from "components/analytics";
// types
import { IAnalyticsParams } from "types";
import { IAnalyticsParams } from "@plane/types";
// 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 { renderMonthAndYear } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
type Props = {
datum: BarTooltipProps<any>;
@ -60,8 +60,8 @@ export const CustomTooltip: React.FC<Props> = ({ datum, analytics, params }) =>
? "capitalize"
: ""
: params.x_axis === "priority" || params.x_axis === "state__group"
? "capitalize"
: ""
? "capitalize"
: ""
}`}
>
{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 { generateBarColor, generateDisplayName } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
import { IAnalyticsParams, IAnalyticsResponse } from "@plane/types";
type Props = {
analytics: IAnalyticsResponse;
@ -101,8 +101,8 @@ export const AnalyticsGraph: React.FC<Props> = ({ analytics, barGraphData, param
? generateDisplayName(datum.value, analytics, params, "x_axis")[0].toUpperCase()
: "?"
: datum.value && datum.value !== "None"
? `${datum.value}`.toUpperCase()[0]
: "?"}
? `${datum.value}`.toUpperCase()[0]
: "?"}
</text>
</g>
</Tooltip>

View File

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

View File

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

View File

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

View File

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

View File

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

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