Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c55c021a37 | ||
|
|
d3e4eb5753 | ||
|
|
a187e7765c | ||
|
|
4c333d5767 | ||
|
|
b317a14983 | ||
|
|
19a28ea9d5 | ||
|
|
6e0999c35a | ||
|
|
52b57b1e37 | ||
|
|
88a35efa06 | ||
|
|
d38594376b | ||
|
|
dae8ca6053 | ||
|
|
6d3bd78052 | ||
|
|
1ad99873a9 | ||
|
|
372074fce1 | ||
|
|
7a09db7d1f | ||
|
|
5153744a35 | ||
|
|
1283d7597d | ||
|
|
7db78594dc | ||
|
|
5e8d523ed4 | ||
|
|
de7a672b79 | ||
|
|
06722408b8 | ||
|
|
0e96eddb57 | ||
|
|
afa10d7195 | ||
|
|
3b8a8cdfa4 | ||
|
|
68c8741f93 | ||
|
|
3440fb962d | ||
|
|
3a7ea55e81 | ||
|
|
e8d303dd10 | ||
|
|
c9a6380636 | ||
|
|
1aadbee7e2 | ||
|
|
02060f654c | ||
|
|
771ca585db | ||
|
|
f6be3dc690 | ||
|
|
8c294e8d5d | ||
|
|
e87890f8ac | ||
|
|
8dbd77974b | ||
|
|
b297241d34 | ||
|
|
daa0b16960 | ||
|
|
a3a2fb84a3 | ||
|
|
0005ff5f99 | ||
|
|
0c7b7c4e94 | ||
|
|
4d835c5b4a | ||
|
|
c7092edb61 | ||
|
|
73afb8f4d8 | ||
|
|
978909c021 | ||
|
|
de9f34cac3 | ||
|
|
e3793f4464 | ||
|
|
1621125f6d | ||
|
|
bd077e6500 | ||
|
|
6af2aa666a | ||
|
|
60ae940d40 | ||
|
|
cdfff12f4f | ||
|
|
e01a0d20fe | ||
|
|
63c4792e70 | ||
|
|
ce562fa3ea | ||
|
|
a6a0eb9774 | ||
|
|
d603c1e8f0 | ||
|
|
405ef9314f | ||
|
|
7cd5112e8f | ||
|
|
8ef9df28c4 | ||
|
|
926d2ae0a0 | ||
|
|
11258686ad | ||
|
|
ed67a64e79 | ||
|
|
e6e2b4e096 | ||
|
|
01a0b34ee9 | ||
|
|
74b764d8d4 | ||
|
|
2e05a2199e | ||
|
|
3a1d945f30 | ||
|
|
16f31f7ba1 |
@@ -33,14 +33,9 @@ jobs:
|
||||
deploy:
|
||||
- space/**
|
||||
|
||||
- name: Setup .npmrc for repository
|
||||
run: |
|
||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
||||
|
||||
- name: Build Plane's Main App
|
||||
if: steps.changed-files.outputs.web_any_changed == 'true'
|
||||
run: |
|
||||
mv ./.npmrc ./web
|
||||
cd web
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
@@ -22,10 +22,6 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Setup .npmrc for repository
|
||||
run: |
|
||||
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaFrontend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
|
||||
+58
-16
@@ -8,8 +8,8 @@ Before submitting a new issue, please search the [issues](https://github.com/mak
|
||||
|
||||
While we want to fix all the [issues](https://github.com/makeplane/plane/issues), before fixing a bug we need to be able to reproduce and confirm it. Please provide us with a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us the information without asking questions back & forth with additional questions like:
|
||||
|
||||
- 3rd-party libraries being used and their versions
|
||||
- a use-case that fails
|
||||
- 3rd-party libraries being used and their versions
|
||||
- a use-case that fails
|
||||
|
||||
Without said minimal reproduction, we won't be able to investigate all [issues](https://github.com/makeplane/plane/issues), and the issue might not be resolved.
|
||||
|
||||
@@ -19,10 +19,10 @@ You can open a new issue with this [issue form](https://github.com/makeplane/pla
|
||||
|
||||
### Requirements
|
||||
|
||||
- Node.js version v16.18.0
|
||||
- Python version 3.8+
|
||||
- Postgres version v14
|
||||
- Redis version v6.2.7
|
||||
- Node.js version v16.18.0
|
||||
- Python version 3.8+
|
||||
- Postgres version v14
|
||||
- Redis version v6.2.7
|
||||
|
||||
### Setup the project
|
||||
|
||||
@@ -30,6 +30,48 @@ The project is a monorepo, with backend api and frontend in a single repo.
|
||||
|
||||
The backend is a django project which is kept inside apiserver
|
||||
|
||||
1. Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/makeplane/plane
|
||||
cd plane
|
||||
chmod +x setup.sh
|
||||
```
|
||||
|
||||
2. Run setup.sh
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
3. Define `NEXT_PUBLIC_API_BASE_URL=http://localhost` in **web/.env** and **space/.env** file
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./web/.env
|
||||
```
|
||||
|
||||
```bash
|
||||
echo "\nNEXT_PUBLIC_API_BASE_URL=http://localhost\n" >> ./space/.env
|
||||
```
|
||||
|
||||
4. Run Docker compose up
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
6. Run the web app in development mode
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Missing a Feature?
|
||||
|
||||
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
|
||||
@@ -39,8 +81,8 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
|
||||
|
||||
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
||||
|
||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||
- We use [Eslint default rule guide](https://eslint.org/docs/rules/), with minor changes. An automated formatter is available using prettier.
|
||||
|
||||
## Need help? Questions and suggestions
|
||||
|
||||
@@ -48,11 +90,11 @@ Questions, suggestions, and thoughts are most welcome. We can also be reached in
|
||||
|
||||
## Ways to contribute
|
||||
|
||||
- Try Plane Cloud and the self hosting platform and give feedback
|
||||
- Add new integrations
|
||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||
- Share your thoughts and suggestions with us
|
||||
- Help create tutorials and blog posts
|
||||
- Request a feature by submitting a proposal
|
||||
- Report a bug
|
||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||
- Try Plane Cloud and the self hosting platform and give feedback
|
||||
- Add new integrations
|
||||
- Help with open [issues](https://github.com/makeplane/plane/issues) or [create your own](https://github.com/makeplane/plane/issues/new/choose)
|
||||
- Share your thoughts and suggestions with us
|
||||
- Help create tutorials and blog posts
|
||||
- Request a feature by submitting a proposal
|
||||
- Report a bug
|
||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||
|
||||
@@ -59,17 +59,6 @@ chmod +x setup.sh
|
||||
|
||||
> If running in a cloud env replace localhost with public facing IP address of the VM
|
||||
|
||||
- Setup Tiptap Pro
|
||||
|
||||
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
|
||||
|
||||
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
|
||||
|
||||
```
|
||||
@tiptap-pro:registry=https://registry.tiptap.dev/
|
||||
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
|
||||
```
|
||||
|
||||
- Run Docker compose up
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
|
||||
@@ -23,7 +23,7 @@ from .project import (
|
||||
ProjectPublicMemberSerializer
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
|
||||
from .asset import FileAssetSerializer
|
||||
from .issue import (
|
||||
|
||||
@@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer):
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
assignees = serializers.SerializerMethodField(read_only=True)
|
||||
labels = serializers.SerializerMethodField(read_only=True)
|
||||
total_estimates = serializers.IntegerField(read_only=True)
|
||||
completed_estimates = serializers.IntegerField(read_only=True)
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
@@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer):
|
||||
members = [
|
||||
{
|
||||
"avatar": assignee.avatar,
|
||||
"first_name": assignee.first_name,
|
||||
"display_name": assignee.display_name,
|
||||
"id": assignee.id,
|
||||
}
|
||||
for issue_cycle in obj.issue_cycle.all()
|
||||
for issue_cycle in obj.issue_cycle.prefetch_related("issue__assignees").all()
|
||||
for assignee in issue_cycle.issue.assignees.all()
|
||||
]
|
||||
# Use a set comprehension to return only the unique objects
|
||||
@@ -64,24 +62,6 @@ class CycleSerializer(BaseSerializer):
|
||||
unique_list = [dict(item) for item in unique_objects]
|
||||
|
||||
return unique_list
|
||||
|
||||
def get_labels(self, obj):
|
||||
labels = [
|
||||
{
|
||||
"name": label.name,
|
||||
"color": label.color,
|
||||
"id": label.id,
|
||||
}
|
||||
for issue_cycle in obj.issue_cycle.all()
|
||||
for label in issue_cycle.issue.labels.all()
|
||||
]
|
||||
# Use a set comprehension to return only the unique objects
|
||||
unique_objects = {frozenset(item.items()) for item in labels}
|
||||
|
||||
# Convert the set back to a list of dictionaries
|
||||
unique_list = [dict(item) for item in unique_objects]
|
||||
|
||||
return unique_list
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
|
||||
@@ -5,10 +5,39 @@ from rest_framework import serializers
|
||||
from .base import BaseSerializer
|
||||
from .workspace import WorkspaceLiteSerializer
|
||||
from .project import ProjectLiteSerializer
|
||||
from plane.db.models import IssueView, IssueViewFavorite
|
||||
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
|
||||
class GlobalViewSerializer(BaseSerializer):
|
||||
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = GlobalView
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"query",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
return GlobalView.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
query_params = validated_data.get("query_data", {})
|
||||
if bool(query_params):
|
||||
validated_data["query"] = issue_filters(query_params, "POST")
|
||||
else:
|
||||
validated_data["query"] = dict()
|
||||
validated_data["query"] = issue_filters(query_params, "PATCH")
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class IssueViewSerializer(BaseSerializer):
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||
|
||||
@@ -102,6 +102,8 @@ from plane.api.views import (
|
||||
BulkEstimatePointEndpoint,
|
||||
## End Estimates
|
||||
# Views
|
||||
GlobalViewViewSet,
|
||||
GlobalViewIssuesViewSet,
|
||||
IssueViewViewSet,
|
||||
ViewIssuesEndpoint,
|
||||
IssueViewFavoriteViewSet,
|
||||
@@ -184,7 +186,6 @@ from plane.api.views import (
|
||||
## Exporter
|
||||
ExportIssuesEndpoint,
|
||||
## End Exporter
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -241,7 +242,11 @@ urlpatterns = [
|
||||
UpdateUserTourCompletedEndpoint.as_view(),
|
||||
name="user-tour",
|
||||
),
|
||||
path("users/workspaces/<str:slug>/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||
path(
|
||||
"users/workspaces/<str:slug>/activities/",
|
||||
UserActivityEndpoint.as_view(),
|
||||
name="user-activities",
|
||||
),
|
||||
# user workspaces
|
||||
path(
|
||||
"users/me/workspaces/",
|
||||
@@ -649,6 +654,37 @@ urlpatterns = [
|
||||
ViewIssuesEndpoint.as_view(),
|
||||
name="project-view-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/",
|
||||
GlobalViewViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/views/<uuid:pk>/",
|
||||
GlobalViewViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"put": "update",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="global-view",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
GlobalViewIssuesViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
}
|
||||
),
|
||||
name="global-view-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
|
||||
IssueViewFavoriteViewSet.as_view(
|
||||
@@ -767,11 +803,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/issues/",
|
||||
WorkSpaceIssuesEndpoint.as_view(),
|
||||
name="workspace-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
LabelViewSet.as_view(
|
||||
|
||||
@@ -56,7 +56,7 @@ from .workspace import (
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
from .cycle import (
|
||||
CycleViewSet,
|
||||
CycleIssueViewSet,
|
||||
|
||||
@@ -80,6 +80,7 @@ class CycleViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
@@ -101,48 +102,84 @@ class CycleViewSet(BaseViewSet):
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.annotate(is_favorite=Exists(subquery))
|
||||
.annotate(total_issues=Count("issue_cycle"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(issue_cycle__issue__state__group="cancelled"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(issue_cycle__issue__state__group="started"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(issue_cycle__issue__state__group="unstarted"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(issue_cycle__issue__state__group="backlog"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(issue_cycle__issue__state__group="completed"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(issue_cycle__issue__state__group="started"),
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@@ -195,17 +232,30 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("display_name", "assignee_id", "avatar")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
@@ -221,17 +271,30 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(total_issues=Count("label_id"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
@@ -384,17 +447,30 @@ class CycleViewSet(BaseViewSet):
|
||||
.values(
|
||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||
)
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("first_name", "last_name")
|
||||
@@ -411,17 +487,30 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(total_issues=Count("label_id"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(archived_at__isnull=True, is_draft=False),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
@@ -487,6 +576,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -662,6 +752,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
# Return all Cycle Issues
|
||||
|
||||
@@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
||||
sort_order=largest_sort_order,
|
||||
start_date=issue_data.get("start_date", None),
|
||||
target_date=issue_data.get("target_date", None),
|
||||
priority=issue_data.get("priority", None),
|
||||
priority=issue_data.get("priority", "none"),
|
||||
created_by=request.user,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Check for valid priority
|
||||
if not request.data.get("issue", {}).get("priority", None) in [
|
||||
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
None,
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
@@ -213,6 +213,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
@@ -277,6 +278,7 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
@@ -478,12 +480,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Check for valid priority
|
||||
if not request.data.get("issue", {}).get("priority", None) in [
|
||||
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
None,
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
@@ -518,6 +520,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
@@ -582,6 +585,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_serializer.save()
|
||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -4,6 +4,7 @@ import random
|
||||
from itertools import chain
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
OuterRef,
|
||||
@@ -129,6 +130,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
@@ -149,6 +151,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -315,6 +318,7 @@ class IssueViewSet(BaseViewSet):
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -326,7 +330,12 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.issue_objects.get(
|
||||
issue = Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.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)
|
||||
@@ -568,6 +577,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -586,6 +596,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
IssueCommentSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
@@ -607,6 +618,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
IssueCommentSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -890,6 +902,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("issue_id")),
|
||||
project_id=str(self.kwargs.get("project_id")),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -908,6 +921,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
IssueLinkSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
@@ -929,6 +943,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
IssueLinkSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -1007,6 +1022,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer.data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1029,6 +1045,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1231,6 +1248,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
@@ -1435,6 +1453,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
@@ -1458,6 +1477,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
"identifier": str(issue_reaction.id),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1506,6 +1526,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
@@ -1530,6 +1551,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
"comment_id": str(comment_id),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1626,6 +1648,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
if not ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
@@ -1675,6 +1698,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
IssueCommentSerializer(comment).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1708,6 +1732,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
IssueCommentSerializer(comment).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
comment.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1782,6 +1807,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1826,6 +1852,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
"identifier": str(issue_reaction.id),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -1899,6 +1926,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -1950,6 +1978,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
"comment_id": str(comment_id),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -2013,6 +2042,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
serializer = IssueVoteSerializer(issue_vote)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
@@ -2047,6 +2077,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
"identifier": str(issue_vote.id),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
issue_vote.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -2080,6 +2111,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
IssueRelationSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -2113,6 +2145,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
if relation == "blocking":
|
||||
@@ -2157,6 +2190,8 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.select_related("issue")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class IssueRetrievePublicEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
AllowAny,
|
||||
@@ -2382,6 +2417,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
@@ -2566,6 +2602,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import json
|
||||
|
||||
# Django Imports
|
||||
from django.utils import timezone
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
||||
from django.core import serializers
|
||||
@@ -39,6 +40,7 @@ from plane.utils.grouper import group_results
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
model = Module
|
||||
permission_classes = [
|
||||
@@ -77,35 +79,63 @@ class ModuleViewSet(BaseViewSet):
|
||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("issue_module"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_module",
|
||||
filter=Q(
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(issue_module__issue__state__group="completed"),
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="completed",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(issue_module__issue__state__group="cancelled"),
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="cancelled",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(issue_module__issue__state__group="started"),
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="started",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(issue_module__issue__state__group="unstarted"),
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="unstarted",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(issue_module__issue__state__group="backlog"),
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="backlog",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(order_by, "name")
|
||||
@@ -129,6 +159,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_destroy(instance)
|
||||
@@ -177,18 +208,36 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.values(
|
||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"assignee_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("first_name", "last_name")
|
||||
@@ -204,17 +253,33 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(total_issues=Count("label_id"))
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=False),
|
||||
filter=Q(
|
||||
completed_at__isnull=False,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
pending_issues=Count(
|
||||
"label_id",
|
||||
filter=Q(completed_at__isnull=True),
|
||||
filter=Q(
|
||||
completed_at__isnull=True,
|
||||
archived_at__isnull=True,
|
||||
is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by("label_name")
|
||||
@@ -277,6 +342,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
@@ -444,6 +510,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
}
|
||||
),
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return Response(
|
||||
@@ -490,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
||||
|
||||
|
||||
class ModuleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = ModuleFavoriteSerializer
|
||||
model = ModuleFavorite
|
||||
|
||||
|
||||
@@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
).select_related("project", "member")
|
||||
).select_related("project", "member", "workspace")
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
# Django imports
|
||||
from django.db.models import (
|
||||
Prefetch,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Case,
|
||||
Value,
|
||||
CharField,
|
||||
When,
|
||||
Exists,
|
||||
Max,
|
||||
)
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, OuterRef, Exists
|
||||
|
||||
@@ -10,18 +24,192 @@ from sentry_sdk import capture_exception
|
||||
# Module imports
|
||||
from . import BaseViewSet, BaseAPIView
|
||||
from plane.api.serializers import (
|
||||
GlobalViewSerializer,
|
||||
IssueViewSerializer,
|
||||
IssueLiteSerializer,
|
||||
IssueViewFavoriteSerializer,
|
||||
)
|
||||
from plane.api.permissions import ProjectEntityPermission
|
||||
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
GlobalView,
|
||||
IssueView,
|
||||
Issue,
|
||||
IssueViewFavorite,
|
||||
IssueReaction,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.grouper import group_results
|
||||
|
||||
|
||||
class GlobalViewViewSet(BaseViewSet):
|
||||
serializer_class = GlobalViewSerializer
|
||||
model = GlobalView
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace")
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
||||
class GlobalViewIssuesViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
WorkspaceEntityPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("state")
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
def list(self, request, slug):
|
||||
try:
|
||||
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)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
attachment_count=IssueAttachment.objects.filter(
|
||||
issue=OuterRef("id")
|
||||
)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order
|
||||
if order_by_param == "priority"
|
||||
else priority_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
||||
# State Ordering
|
||||
elif order_by_param in [
|
||||
"state__name",
|
||||
"state__group",
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order")
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"-labels__name",
|
||||
"assignees__first_name",
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
issues = IssueLiteSerializer(issue_queryset, many=True).data
|
||||
|
||||
## Grouping the results
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
if sub_group_by and sub_group_by == group_by:
|
||||
return Response(
|
||||
{"error": "Group by and sub group by cannot be same"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if group_by:
|
||||
return Response(
|
||||
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
capture_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class IssueViewViewSet(BaseViewSet):
|
||||
|
||||
@@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
created_issues=Count(
|
||||
"project_issue",
|
||||
filter=Q(project_issue__created_by_id=user_id),
|
||||
filter=Q(
|
||||
project_issue__created_by_id=user_id,
|
||||
project_issue__archived_at__isnull=True,
|
||||
project_issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
assigned_issues=Count(
|
||||
"project_issue",
|
||||
filter=Q(project_issue__assignees__in=[user_id]),
|
||||
filter=Q(
|
||||
project_issue__assignees__in=[user_id],
|
||||
project_issue__archived_at__isnull=True,
|
||||
project_issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
filter=Q(
|
||||
project_issue__completed_at__isnull=False,
|
||||
project_issue__assignees__in=[user_id],
|
||||
project_issue__archived_at__isnull=True,
|
||||
project_issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1267,6 +1277,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||
"started",
|
||||
],
|
||||
project_issue__assignees__in=[user_id],
|
||||
project_issue__archived_at__isnull=True,
|
||||
project_issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -1317,6 +1329,11 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, user_id):
|
||||
try:
|
||||
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 = (
|
||||
Issue.issue_objects.filter(
|
||||
|
||||
@@ -32,7 +32,7 @@ def delete_old_s3_link():
|
||||
else:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
region_name="ap-south-1",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=Config(signature_version="s3v4"),
|
||||
|
||||
@@ -39,6 +39,7 @@ def track_name(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("name") != requested_data.get("name"):
|
||||
issue_activities.append(
|
||||
@@ -52,6 +53,7 @@ def track_name(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the name to {requested_data.get('name')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -64,6 +66,7 @@ def track_parent(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("parent") != requested_data.get("parent"):
|
||||
if requested_data.get("parent") == None:
|
||||
@@ -81,6 +84,7 @@ def track_parent(
|
||||
comment=f"updated the parent issue to None",
|
||||
old_identifier=old_parent.id,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -101,6 +105,7 @@ def track_parent(
|
||||
comment=f"updated the parent issue to {new_parent.name}",
|
||||
old_identifier=old_parent.id if old_parent is not None else None,
|
||||
new_identifier=new_parent.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -113,36 +118,23 @@ def track_priority(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("priority") != requested_data.get("priority"):
|
||||
if requested_data.get("priority") == None:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=None,
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the priority to None",
|
||||
)
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=requested_data.get("priority"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||
)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("priority"),
|
||||
new_value=requested_data.get("priority"),
|
||||
field="priority",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track chnages in state of the issue
|
||||
@@ -153,6 +145,7 @@ def track_state(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("state") != requested_data.get("state"):
|
||||
new_state = State.objects.get(pk=requested_data.get("state", None))
|
||||
@@ -171,6 +164,7 @@ def track_state(
|
||||
comment=f"updated the state to {new_state.name}",
|
||||
old_identifier=old_state.id,
|
||||
new_identifier=new_state.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -183,6 +177,7 @@ def track_description(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
@@ -203,6 +198,7 @@ def track_description(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,6 +211,7 @@ def track_target_date(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("target_date") != requested_data.get("target_date"):
|
||||
if requested_data.get("target_date") == None:
|
||||
@@ -229,6 +226,7 @@ def track_target_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the target date to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -243,6 +241,7 @@ def track_target_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the target date to {requested_data.get('target_date')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -255,6 +254,7 @@ def track_start_date(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
if current_instance.get("start_date") != requested_data.get("start_date"):
|
||||
if requested_data.get("start_date") == None:
|
||||
@@ -269,6 +269,7 @@ def track_start_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the start date to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -283,6 +284,7 @@ def track_start_date(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the start date to {requested_data.get('start_date')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -295,6 +297,7 @@ def track_labels(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
# Label Addition
|
||||
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
|
||||
@@ -314,6 +317,7 @@ def track_labels(
|
||||
comment=f"added label {label.name}",
|
||||
new_identifier=label.id,
|
||||
old_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -335,6 +339,7 @@ def track_labels(
|
||||
comment=f"removed label {label.name}",
|
||||
old_identifier=label.id,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -347,6 +352,7 @@ def track_assignees(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
):
|
||||
# Assignee Addition
|
||||
if len(requested_data.get("assignees_list")) > len(
|
||||
@@ -367,6 +373,7 @@ def track_assignees(
|
||||
workspace=project.workspace,
|
||||
comment=f"added assignee {assignee.display_name}",
|
||||
new_identifier=assignee.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -389,12 +396,13 @@ def track_assignees(
|
||||
workspace=project.workspace,
|
||||
comment=f"removed assignee {assignee.display_name}",
|
||||
old_identifier=assignee.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -404,12 +412,13 @@ def create_issue_activity(
|
||||
comment=f"created the issue",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_estimate_points(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
|
||||
if requested_data.get("estimate_point") == None:
|
||||
@@ -424,6 +433,7 @@ def track_estimate_points(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the estimate point to None",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -438,12 +448,13 @@ def track_estimate_points(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_archive_at(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
if requested_data.get("archived_at") is None:
|
||||
issue_activities.append(
|
||||
@@ -457,6 +468,7 @@ def track_archive_at(
|
||||
field="archived_at",
|
||||
old_value="archive",
|
||||
new_value="restore",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -471,12 +483,13 @@ def track_archive_at(
|
||||
field="archived_at",
|
||||
old_value=None,
|
||||
new_value="archive",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def track_closed_to(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
if requested_data.get("closed_to") is not None:
|
||||
updated_state = State.objects.get(
|
||||
@@ -496,12 +509,13 @@ def track_closed_to(
|
||||
comment=f"Plane updated the state to {updated_state.name}",
|
||||
old_identifier=None,
|
||||
new_identifier=updated_state.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
ISSUE_ACTIVITY_MAPPER = {
|
||||
"name": track_name,
|
||||
@@ -518,6 +532,11 @@ def update_issue_activity(
|
||||
"closed_to": track_closed_to,
|
||||
}
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
)
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||
if func is not None:
|
||||
@@ -528,11 +547,12 @@ def update_issue_activity(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -542,12 +562,13 @@ def delete_issue_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="issue",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -566,12 +587,13 @@ def create_comment_activity(
|
||||
new_value=requested_data.get("comment_html", ""),
|
||||
new_identifier=requested_data.get("id", None),
|
||||
issue_comment_id=requested_data.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -593,12 +615,13 @@ def update_comment_activity(
|
||||
new_value=requested_data.get("comment_html", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
issue_comment_id=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -609,12 +632,13 @@ def delete_comment_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="comment",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -646,6 +670,7 @@ def create_cycle_issue_activity(
|
||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -666,12 +691,13 @@ def create_cycle_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"added cycle {cycle.name}",
|
||||
new_identifier=cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_cycle_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -695,12 +721,13 @@ def delete_cycle_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
|
||||
old_identifier=cycle.id if cycle is not None else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -732,6 +759,7 @@ def create_module_issue_activity(
|
||||
comment=f"updated module from {old_module.name} to {new_module.name}",
|
||||
old_identifier=old_module.id,
|
||||
new_identifier=new_module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -751,12 +779,13 @@ def create_module_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=module.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_module_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -780,12 +809,13 @@ def delete_module_issue_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f"removed this issue from {module.name if module is not None else None}",
|
||||
old_identifier=module.id if module is not None else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -803,12 +833,13 @@ def create_link_activity(
|
||||
field="link",
|
||||
new_value=requested_data.get("url", ""),
|
||||
new_identifier=requested_data.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -829,12 +860,13 @@ def update_link_activity(
|
||||
old_identifier=current_instance.get("id"),
|
||||
new_value=requested_data.get("url", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_link_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
|
||||
current_instance = (
|
||||
@@ -851,13 +883,14 @@ def delete_link_activity(
|
||||
actor=actor,
|
||||
field="link",
|
||||
old_value=current_instance.get("url", ""),
|
||||
new_value=""
|
||||
new_value="",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_attachment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -875,12 +908,13 @@ def create_attachment_activity(
|
||||
field="attachment",
|
||||
new_value=current_instance.get("asset", ""),
|
||||
new_identifier=current_instance.get("id", None),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_attachment_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -891,11 +925,12 @@ def delete_attachment_activity(
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
field="attachment",
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
def create_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
@@ -914,12 +949,13 @@ def create_issue_reaction_activity(
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=issue_reaction,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
@@ -938,12 +974,13 @@ def delete_issue_reaction_activity(
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("reaction") is not None:
|
||||
@@ -963,12 +1000,13 @@ def create_comment_reaction_activity(
|
||||
comment="added the reaction",
|
||||
old_identifier=None,
|
||||
new_identifier=comment_reaction_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
@@ -989,12 +1027,13 @@ def delete_comment_reaction_activity(
|
||||
comment="removed the reaction",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data and requested_data.get("vote") is not None:
|
||||
@@ -1011,12 +1050,13 @@ def create_issue_vote_activity(
|
||||
comment="added the vote",
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
current_instance = (
|
||||
json.loads(current_instance) if current_instance is not None else None
|
||||
@@ -1035,12 +1075,13 @@ def delete_issue_vote_activity(
|
||||
comment="removed the vote",
|
||||
old_identifier=current_instance.get("identifier"),
|
||||
new_identifier=None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_relation_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -1080,12 +1121,13 @@ def create_issue_relation_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f'added {issue_relation.get("relation_type")} relation',
|
||||
old_identifier=issue_relation.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_relation_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -1109,6 +1151,7 @@ def delete_issue_relation_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted {relation_type} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
|
||||
@@ -1124,12 +1167,13 @@ def delete_issue_relation_activity(
|
||||
workspace=project.workspace,
|
||||
comment=f'deleted {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_draft_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1140,12 +1184,13 @@ def create_draft_issue_activity(
|
||||
field="draft",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_draft_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
current_instance = (
|
||||
@@ -1160,6 +1205,7 @@ def update_draft_issue_activity(
|
||||
comment=f"created the issue",
|
||||
verb="updated",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -1172,13 +1218,14 @@ def update_draft_issue_activity(
|
||||
field="draft",
|
||||
verb="updated",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def delete_draft_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -1188,6 +1235,7 @@ def delete_draft_issue_activity(
|
||||
field="draft",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1200,6 +1248,7 @@ def issue_activity(
|
||||
issue_id,
|
||||
actor_id,
|
||||
project_id,
|
||||
epoch,
|
||||
subscriber=True,
|
||||
):
|
||||
try:
|
||||
@@ -1276,6 +1325,7 @@ def issue_activity(
|
||||
project,
|
||||
actor,
|
||||
issue_activities,
|
||||
epoch,
|
||||
)
|
||||
|
||||
# Save all the values to database
|
||||
|
||||
@@ -58,27 +58,31 @@ def archive_old_issues():
|
||||
|
||||
# Check if Issues
|
||||
if issues:
|
||||
# Set the archive time to current time
|
||||
archive_at = timezone.now()
|
||||
|
||||
issues_to_update = []
|
||||
for issue in issues:
|
||||
issue.archived_at = timezone.now()
|
||||
issue.archived_at = archive_at
|
||||
issues_to_update.append(issue)
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(
|
||||
Issue.objects.bulk_update(
|
||||
issues_to_update, ["archived_at"], batch_size=100
|
||||
)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
|
||||
requested_data=json.dumps({"archived_at": str(archive_at)}),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
@@ -138,7 +142,7 @@ def close_old_issues():
|
||||
|
||||
# Bulk Update the issues and log the activity
|
||||
if issues_to_update:
|
||||
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
|
||||
[
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
@@ -148,8 +152,9 @@ def close_old_issues():
|
||||
project_id=project_id,
|
||||
current_instance=None,
|
||||
subscriber=False,
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
for issue in updated_issues
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
|
||||
@@ -26,19 +26,19 @@ def workspace_member_props(old_props):
|
||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||
},
|
||||
"display_properties": {
|
||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
||||
"key": old_props.get("properties", {}).get("key", None),
|
||||
"labels": old_props.get("properties", {}).get("labels", None),
|
||||
"link": old_props.get("properties", {}).get("link", None),
|
||||
"priority": old_props.get("properties", {}).get("priority", None),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
||||
"state": old_props.get("properties", {}).get("state", None),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
||||
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||
"key": old_props.get("properties", {}).get("key", True),
|
||||
"labels": old_props.get("properties", {}).get("labels", True),
|
||||
"link": old_props.get("properties", {}).get("link", True),
|
||||
"priority": old_props.get("properties", {}).get("priority", True),
|
||||
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||
"state": old_props.get("properties", {}).get("state", True),
|
||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||
},
|
||||
}
|
||||
return new_props
|
||||
|
||||
@@ -1,23 +1,42 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
IssueActivityModel = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivityModel.objects.all():
|
||||
if obj.field == "blocks":
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
("db", "0044_auto_20230913_0709"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
migrations.CreateModel(
|
||||
name="GlobalView",
|
||||
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,),),
|
||||
("name", models.CharField(max_length=255, verbose_name="View Name")),
|
||||
("description", models.TextField(blank=True, verbose_name="View Description"),),
|
||||
("query", models.JSONField(verbose_name="View Query")),
|
||||
("access", models.PositiveSmallIntegerField(choices=[(0, "Private"), (1, "Public")], default=1),),
|
||||
("query_data", models.JSONField(default=dict)),
|
||||
("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",),),
|
||||
("workspace", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="global_views", to="db.workspace",),),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Global View",
|
||||
"verbose_name_plural": "Global Views",
|
||||
"db_table": "global_views",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="issueactivity",
|
||||
name="epoch",
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-26 10:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.all():
|
||||
obj.epoch = int(obj.created_at.timestamp())
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["epoch"],
|
||||
batch_size=5000,
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_auto_20230915_0655'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-26 10:29
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity_priority(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="priority"):
|
||||
# Set the old and new value to none if it is empty for Priority
|
||||
obj.new_value = obj.new_value or "none"
|
||||
obj.old_value = obj.old_value or "none"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["new_value", "old_value"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
def update_issue_activity_blocked(apps, schema_editor):
|
||||
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.filter(field="blocks"):
|
||||
# Set the field to blocked_by
|
||||
obj.field = "blocked_by"
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(
|
||||
updated_issue_activity,
|
||||
["field"],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0046_auto_20230926_1015'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_issue_activity_priority),
|
||||
migrations.RunPython(update_issue_activity_blocked),
|
||||
]
|
||||
@@ -50,7 +50,7 @@ from .state import State
|
||||
|
||||
from .cycle import Cycle, CycleIssue, CycleFavorite
|
||||
|
||||
from .view import IssueView, IssueViewFavorite
|
||||
from .view import GlobalView, IssueView, IssueViewFavorite
|
||||
|
||||
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
|
||||
|
||||
|
||||
@@ -309,6 +309,7 @@ class IssueActivity(ProjectBaseModel):
|
||||
)
|
||||
old_identifier = models.UUIDField(null=True)
|
||||
new_identifier = models.UUIDField(null=True)
|
||||
epoch = models.FloatField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue Activity"
|
||||
|
||||
@@ -26,7 +26,7 @@ ROLE_CHOICES = (
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": None,
|
||||
"priority": "none",
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
|
||||
@@ -3,7 +3,30 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
# Module import
|
||||
from . import ProjectBaseModel
|
||||
from . import ProjectBaseModel, BaseModel
|
||||
|
||||
|
||||
class GlobalView(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
|
||||
)
|
||||
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")
|
||||
access = models.PositiveSmallIntegerField(
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
query_data = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Global View"
|
||||
verbose_name_plural = "Global Views"
|
||||
db_table = "global_views"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the View"""
|
||||
return f"{self.name} <{self.workspace.name}>"
|
||||
|
||||
|
||||
class IssueView(ProjectBaseModel):
|
||||
|
||||
@@ -17,7 +17,7 @@ ROLE_CHOICES = (
|
||||
def get_default_props():
|
||||
return {
|
||||
"filters": {
|
||||
"priority": None,
|
||||
"priority": "none",
|
||||
"state": None,
|
||||
"state_group": None,
|
||||
"assignees": None,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""Production settings and globals."""
|
||||
from urllib.parse import urlparse
|
||||
import ssl
|
||||
import certifi
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
@@ -91,112 +89,89 @@ if bool(os.environ.get("SENTRY_DSN", False)):
|
||||
profiles_sample_rate=1.0,
|
||||
)
|
||||
|
||||
if DOCKERIZED and USE_MINIO:
|
||||
INSTALLED_APPS += ("storages",)
|
||||
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||
# The name of the bucket to store files in.
|
||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||
)
|
||||
# Default permissions
|
||||
AWS_DEFAULT_ACL = "public-read"
|
||||
AWS_QUERYSTRING_AUTH = False
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
|
||||
# Custom Domain settings
|
||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||
else:
|
||||
# The AWS region to connect to.
|
||||
AWS_REGION = os.environ.get("AWS_REGION", "")
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
|
||||
# The optional AWS session token to use.
|
||||
# AWS_SESSION_TOKEN = ""
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
|
||||
# The name of the bucket to store files in.
|
||||
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
|
||||
# How to construct S3 URLs ("auto", "path", "virtual").
|
||||
AWS_S3_ADDRESSING_STYLE = "auto"
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
|
||||
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
|
||||
AWS_S3_KEY_PREFIX = ""
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
|
||||
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
|
||||
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
|
||||
# and their permissions will be set to "public-read".
|
||||
AWS_S3_BUCKET_AUTH = False
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
|
||||
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
|
||||
# is True. It also affects the "Cache-Control" header of the files.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
|
||||
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
|
||||
# cannot be used with `AWS_S3_BUCKET_AUTH`.
|
||||
AWS_S3_PUBLIC_URL = ""
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
|
||||
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
|
||||
# understand the consequences before enabling.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_REDUCED_REDUNDANCY = False
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
|
||||
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_DISPOSITION = ""
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
|
||||
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_CONTENT_LANGUAGE = ""
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
|
||||
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
|
||||
# single `name` argument.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_METADATA = {}
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
|
||||
# If True, then files will be stored using AES256 server-side encryption.
|
||||
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
|
||||
# Otherwise, server-side encryption is not be enabled.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_ENCRYPT_KEY = False
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
|
||||
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
|
||||
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
|
||||
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
|
||||
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
|
||||
# compressed size is smaller than their uncompressed size.
|
||||
# Important: Changing this setting will not affect existing files.
|
||||
AWS_S3_GZIP = True
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
|
||||
# The signature version to use for S3 requests.
|
||||
AWS_S3_SIGNATURE_VERSION = None
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
# If True, then files with the same name will overwrite each other. By default it's set to False to have
|
||||
# extra characters appended.
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
STORAGES["default"] = {
|
||||
"BACKEND": "django_s3_storage.storage.S3Storage",
|
||||
}
|
||||
|
||||
# AWS Settings End
|
||||
|
||||
@@ -218,27 +193,16 @@ CSRF_COOKIE_SECURE = True
|
||||
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
if DOCKERIZED:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
}
|
||||
}
|
||||
else:
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
|
||||
@@ -261,19 +225,16 @@ broker_url = (
|
||||
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
)
|
||||
|
||||
if DOCKERIZED:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
else:
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
CELERY_BROKER_URL = broker_url
|
||||
|
||||
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
|
||||
|
||||
|
||||
# Enable or Disable signups
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||
|
||||
# Scout Settings
|
||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
|
||||
SCOUT_NAME = "Plane"
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Self hosted settings and globals."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dj_database_url
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
from .common import * # noqa
|
||||
|
||||
# Database
|
||||
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
|
||||
|
||||
# Docker configurations
|
||||
DOCKERIZED = 1
|
||||
USE_MINIO = 1
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "plane",
|
||||
"USER": os.environ.get("PGUSER", ""),
|
||||
"PASSWORD": os.environ.get("PGPASSWORD", ""),
|
||||
"HOST": os.environ.get("PGHOST", ""),
|
||||
}
|
||||
}
|
||||
|
||||
# Parse database configuration from $DATABASE_URL
|
||||
DATABASES["default"] = dj_database_url.config()
|
||||
SITE_ID = 1
|
||||
|
||||
# File size limit
|
||||
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
|
||||
|
||||
CORS_ALLOW_METHODS = [
|
||||
"DELETE",
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"PUT",
|
||||
]
|
||||
|
||||
CORS_ALLOW_HEADERS = [
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"authorization",
|
||||
"content-type",
|
||||
"dnt",
|
||||
"origin",
|
||||
"user-agent",
|
||||
"x-csrftoken",
|
||||
"x-requested-with",
|
||||
]
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
INSTALLED_APPS += ("storages",)
|
||||
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
|
||||
# The AWS access key to use.
|
||||
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
|
||||
# The AWS secret access key to use.
|
||||
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
|
||||
# The name of the bucket to store files in.
|
||||
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
|
||||
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
|
||||
AWS_S3_ENDPOINT_URL = os.environ.get(
|
||||
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
|
||||
)
|
||||
# Default permissions
|
||||
AWS_DEFAULT_ACL = "public-read"
|
||||
AWS_QUERYSTRING_AUTH = False
|
||||
AWS_S3_FILE_OVERWRITE = False
|
||||
|
||||
# Custom Domain settings
|
||||
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
|
||||
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
|
||||
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Allow all host headers
|
||||
ALLOWED_HOSTS = [
|
||||
"*",
|
||||
]
|
||||
|
||||
# Security settings
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Redis URL
|
||||
REDIS_URL = os.environ.get("REDIS_URL")
|
||||
|
||||
# Caches
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": REDIS_URL,
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# URL used for email redirects
|
||||
WEB_URL = os.environ.get("WEB_URL", "http://localhost")
|
||||
|
||||
# Celery settings
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
|
||||
# Enable or Disable signups
|
||||
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
|
||||
|
||||
# Analytics
|
||||
ANALYTICS_BASE_API = False
|
||||
|
||||
# OPEN AI Settings
|
||||
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
|
||||
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
|
||||
@@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||
|
||||
sorted_data = grouped_data
|
||||
if temp_axis == "priority":
|
||||
order = ["low", "medium", "high", "urgent", "None"]
|
||||
order = ["low", "medium", "high", "urgent", "none"]
|
||||
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
||||
else:
|
||||
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0])))
|
||||
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "none", x[0])))
|
||||
return sorted_data
|
||||
|
||||
|
||||
|
||||
@@ -40,9 +40,6 @@ def filter_priority(params, filter, method):
|
||||
priorities = params.get("priority").split(",")
|
||||
if len(priorities) and "" not in priorities:
|
||||
filter["priority__in"] = priorities
|
||||
else:
|
||||
if params.get("priority", None) and len(params.get("priority")):
|
||||
filter["priority__in"] = params.get("priority")
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
+28
-79
@@ -1,113 +1,61 @@
|
||||
version: "3.8"
|
||||
|
||||
x-api-and-worker-env:
|
||||
&api-and-worker-env
|
||||
DEBUG: ${DEBUG}
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
DJANGO_SETTINGS_MODULE: plane.settings.production
|
||||
DATABASE_URL: postgres://${PGUSER}:${PGPASSWORD}@${PGHOST}:5432/${PGDATABASE}
|
||||
REDIS_URL: redis://plane-redis:6379/
|
||||
EMAIL_HOST: ${EMAIL_HOST}
|
||||
EMAIL_HOST_USER: ${EMAIL_HOST_USER}
|
||||
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD}
|
||||
EMAIL_PORT: ${EMAIL_PORT}
|
||||
EMAIL_FROM: ${EMAIL_FROM}
|
||||
EMAIL_USE_TLS: ${EMAIL_USE_TLS}
|
||||
EMAIL_USE_SSL: ${EMAIL_USE_SSL}
|
||||
AWS_REGION: ${AWS_REGION}
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME}
|
||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT}
|
||||
WEB_URL: ${WEB_URL}
|
||||
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
|
||||
DISABLE_COLLECTSTATIC: 1
|
||||
DOCKERIZED: 1
|
||||
OPENAI_API_BASE: ${OPENAI_API_BASE}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
GPT_ENGINE: ${GPT_ENGINE}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
DEFAULT_EMAIL: ${DEFAULT_EMAIL}
|
||||
DEFAULT_PASSWORD: ${DEFAULT_PASSWORD}
|
||||
USE_MINIO: ${USE_MINIO}
|
||||
ENABLE_SIGNUP: ${ENABLE_SIGNUP}
|
||||
|
||||
services:
|
||||
plane-web:
|
||||
container_name: planefrontend
|
||||
web:
|
||||
container_name: web
|
||||
image: makeplane/plane-frontend:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
NEXT_PUBLIC_DEPLOY_URL: ${NEXT_PUBLIC_DEPLOY_URL}
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID: "0"
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME: "0"
|
||||
NEXT_PUBLIC_GITHUB_ID: "0"
|
||||
NEXT_PUBLIC_SENTRY_DSN: "0"
|
||||
NEXT_PUBLIC_ENABLE_OAUTH: "0"
|
||||
NEXT_PUBLIC_ENABLE_SENTRY: "0"
|
||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER: "0"
|
||||
NEXT_PUBLIC_TRACK_EVENTS: "0"
|
||||
- ./web/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- api
|
||||
- worker
|
||||
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
image: makeplane/plane-deploy:latest
|
||||
space:
|
||||
container_name: space
|
||||
image: makeplane/plane-space:latest
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
||||
- ./space/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
api:
|
||||
container_name: api
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/takeoff
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
worker:
|
||||
container_name: bgworker
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/worker
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
image: makeplane/plane-backend:latest
|
||||
restart: always
|
||||
command: ./bin/beat
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-and-worker-env
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
@@ -157,8 +105,8 @@ services:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
plane-proxy:
|
||||
container_name: planeproxy
|
||||
proxy:
|
||||
container_name: proxy
|
||||
image: makeplane/plane-proxy:latest
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
@@ -168,8 +116,9 @@ services:
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-web
|
||||
- plane-api
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
+22
-21
@@ -1,8 +1,8 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
plane-web:
|
||||
container_name: planefrontend
|
||||
web:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile.web
|
||||
@@ -11,11 +11,11 @@ services:
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- api
|
||||
- worker
|
||||
|
||||
plane-deploy:
|
||||
container_name: planedeploy
|
||||
space:
|
||||
container_name: space
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./space/Dockerfile.space
|
||||
@@ -24,12 +24,12 @@ services:
|
||||
restart: always
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
depends_on:
|
||||
- plane-api
|
||||
- plane-worker
|
||||
- plane-web
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
plane-api:
|
||||
container_name: planebackend
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
@@ -43,8 +43,8 @@ services:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-worker:
|
||||
container_name: planebgworker
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
@@ -55,12 +55,12 @@ services:
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
plane-beat-worker:
|
||||
container_name: planebeatworker
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apiserver
|
||||
dockerfile: Dockerfile.api
|
||||
@@ -71,7 +71,7 @@ services:
|
||||
env_file:
|
||||
- ./apiserver/.env
|
||||
depends_on:
|
||||
- plane-api
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
@@ -118,8 +118,8 @@ services:
|
||||
- plane-minio
|
||||
|
||||
# Comment this if you already have a reverse proxy running
|
||||
plane-proxy:
|
||||
container_name: planeproxy
|
||||
proxy:
|
||||
container_name: proxy
|
||||
build:
|
||||
context: ./nginx
|
||||
dockerfile: Dockerfile
|
||||
@@ -130,8 +130,9 @@ services:
|
||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- plane-web
|
||||
- plane-api
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
events { }
|
||||
events {
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://planefrontend:3000/;
|
||||
proxy_pass http://web:3000/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://planebackend:8000/api/;
|
||||
proxy_pass http://api:8000/api/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
proxy_pass http://planedeploy:3000/spaces/;
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_pass http://space:3000/spaces/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,4 @@ cp ./space/.env.example ./space/.env
|
||||
cp ./apiserver/.env.example ./apiserver/.env
|
||||
|
||||
# Generate the SECRET_KEY that will be used by django
|
||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
|
||||
# Generate Prompt for taking tiptap auth key
|
||||
echo -e "\n\e[1;38m Instructions for generating TipTap Pro Extensions Auth Token \e[0m \n"
|
||||
|
||||
echo -e "\e[1;38m 1. Head over to TipTap cloud's Pro Extensions Page, https://collab.tiptap.dev/pro-extensions \e[0m"
|
||||
echo -e "\e[1;38m 2. Copy the token given to you under the first paragraph, after 'Here it is' \e[0m \n"
|
||||
|
||||
read -p $'\e[1;32m Please Enter Your TipTap Pro Extensions Authentication Token: \e[0m \e[1;36m' authToken
|
||||
|
||||
echo "@tiptap-pro:registry=https://registry.tiptap.dev/
|
||||
//registry.tiptap.dev/:_authToken=${authToken}" > .npmrc
|
||||
echo -e "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env
|
||||
@@ -1,4 +1,2 @@
|
||||
# Google Client ID for Google OAuth
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||
# Flag to toggle OAuth
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
@@ -33,7 +33,7 @@ export const SignInView = observer(() => {
|
||||
const onSignInSuccess = (response: any) => {
|
||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
||||
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/";
|
||||
const nextPath = router.asPath.includes("next_path") ? router.asPath.split("/?next_path=")[1] : "/login";
|
||||
|
||||
userStore.setCurrentUser(response?.user);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SignInView = observer(() => {
|
||||
router.push(`/onboarding?next_path=${nextPath}`);
|
||||
return;
|
||||
}
|
||||
router.push((nextPath ?? "/").toString());
|
||||
router.push((nextPath ?? "/login").toString());
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
import "highlight.js/styles/github-dark.css";
|
||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||
import UpdatedImage from "./updated-image";
|
||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||
import { CustomTableCell } from "./table/table-cell";
|
||||
@@ -121,9 +120,6 @@ export const TiptapExtensions = (
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
UniqueID.configure({
|
||||
types: ["image"],
|
||||
}),
|
||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "./home";
|
||||
export * from "./login";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// components
|
||||
import { SignInView, UserLoggedIn } from "components/accounts";
|
||||
|
||||
export const HomeView = observer(() => {
|
||||
export const LoginView = observer(() => {
|
||||
const { user: userStore } = useMobxStore();
|
||||
|
||||
if (!userStore.currentUser) return <SignInView />;
|
||||
@@ -17,7 +17,6 @@
|
||||
"@heroicons/react": "^2.0.12",
|
||||
"@mui/icons-material": "^5.14.1",
|
||||
"@mui/material": "^5.14.1",
|
||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
"@tiptap/extension-gapcursor": "^2.1.7",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { HomeView } from "components/views";
|
||||
|
||||
const HomePage = () => <HomeView />;
|
||||
|
||||
export default HomePage;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LoginView } from "components/views";
|
||||
|
||||
const LoginPage = () => <LoginView />;
|
||||
|
||||
export default LoginPage;
|
||||
+1
-21
@@ -1,24 +1,4 @@
|
||||
# Extra image domains that need to be added for Next Image
|
||||
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
|
||||
# Google Client ID for Google OAuth
|
||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
||||
# GitHub App ID for GitHub OAuth
|
||||
NEXT_PUBLIC_GITHUB_ID=""
|
||||
# GitHub App Name for GitHub Integration
|
||||
NEXT_PUBLIC_GITHUB_APP_NAME=""
|
||||
# Sentry DSN for error monitoring
|
||||
NEXT_PUBLIC_SENTRY_DSN=""
|
||||
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
||||
# Enable/Disable Sentry
|
||||
NEXT_PUBLIC_ENABLE_SENTRY=0
|
||||
# Enable/Disable session recording
|
||||
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
|
||||
# Enable/Disable event tracking
|
||||
NEXT_PUBLIC_TRACK_EVENTS=0
|
||||
# Slack Client ID for Slack Integration
|
||||
NEXT_PUBLIC_SLACK_CLIENT_ID=""
|
||||
# For Telemetry, set it to "app.plane.so"
|
||||
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
|
||||
# Public boards deploy URL
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost:3000/spaces"
|
||||
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
|
||||
@@ -9,7 +9,6 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
|
||||
import { generateBarColor } from "helpers/analytics.helper";
|
||||
// types
|
||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
analytics: IAnalyticsResponse;
|
||||
|
||||
@@ -161,6 +161,7 @@ export const CommandPalette: React.FC = observer(() => {
|
||||
/>
|
||||
<CreateUpdateViewModal
|
||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
||||
viewType="project"
|
||||
isOpen={isCreateViewModalOpen}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// icons
|
||||
import { Icon, Tooltip } from "components/ui";
|
||||
import { CopyPlus } from "lucide-react";
|
||||
@@ -10,6 +14,8 @@ import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "types";
|
||||
// fetch-keys
|
||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
const router = useRouter();
|
||||
@@ -52,6 +58,26 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LabelPill = ({ labelId }: { labelId: string }) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: labels } = useSWR(
|
||||
workspaceSlug ? WORKSPACE_LABELS(workspaceSlug.toString()) : null,
|
||||
workspaceSlug ? () => issuesService.getWorkspaceLabels(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: labels?.find((l) => l.id === labelId)?.color ?? "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
message: (
|
||||
@@ -325,14 +351,8 @@ const activityDetails: {
|
||||
return (
|
||||
<>
|
||||
added a new label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<LabelPill labelId={activity.new_identifier ?? ""} />
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
@@ -348,13 +368,7 @@ const activityDetails: {
|
||||
<>
|
||||
removed the label{" "}
|
||||
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<LabelPill labelId={activity.old_identifier ?? ""} />
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
</span>
|
||||
{showIssue && (
|
||||
|
||||
@@ -10,7 +10,14 @@ import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||
// helpers
|
||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueFilterOptions, IIssueLabels, IState, IUserLite, TStateGroups } from "types";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueLabels,
|
||||
IProject,
|
||||
IState,
|
||||
IUserLite,
|
||||
TStateGroups,
|
||||
} from "types";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -20,7 +27,9 @@ type Props = {
|
||||
clearAllFilters: (...args: any) => void;
|
||||
labels: IIssueLabels[] | undefined;
|
||||
members: IUserLite[] | undefined;
|
||||
states: IState[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
stateGroup?: string[] | undefined;
|
||||
project?: IProject[] | undefined;
|
||||
};
|
||||
|
||||
export const FiltersList: React.FC<Props> = ({
|
||||
@@ -30,6 +39,7 @@ export const FiltersList: React.FC<Props> = ({
|
||||
labels,
|
||||
members,
|
||||
states,
|
||||
project,
|
||||
}) => {
|
||||
if (!filters) return <></>;
|
||||
|
||||
@@ -155,6 +165,29 @@ export const FiltersList: React.FC<Props> = ({
|
||||
: key === "assignees"
|
||||
? filters.assignees?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
return (
|
||||
<div
|
||||
key={memberId}
|
||||
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1"
|
||||
>
|
||||
<Avatar user={member} />
|
||||
<span>{member?.display_name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "subscriber"
|
||||
? filters.subscriber?.map((memberId: string) => {
|
||||
const member = members?.find((m) => m.id === memberId);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -298,6 +331,30 @@ export const FiltersList: React.FC<Props> = ({
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: key === "project"
|
||||
? filters.project?.map((projectId) => {
|
||||
const currentProject = project?.find((p) => p.id === projectId);
|
||||
console.log("currentProject", currentProject);
|
||||
console.log("currentProject", projectId);
|
||||
return (
|
||||
<p
|
||||
key={currentProject?.id}
|
||||
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||
>
|
||||
<span>{currentProject?.name}</span>
|
||||
<span
|
||||
className="cursor-pointer"
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
project: filters.project?.filter((p) => p !== projectId),
|
||||
})
|
||||
}
|
||||
>
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
: (filters[key] as any)?.join(", ")}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -93,7 +93,9 @@ export const IssuesFilterView: React.FC = () => {
|
||||
<Tooltip
|
||||
key={option.type}
|
||||
tooltipContent={
|
||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
||||
<span className="capitalize">
|
||||
{replaceUnderscoreIfSnakeCase(option.type)} Layout
|
||||
</span>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
@@ -318,7 +320,7 @@ export const IssuesFilterView: React.FC = () => {
|
||||
displayFilters.layout !== "spreadsheet" &&
|
||||
displayFilters.layout !== "gantt_chart" && (
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-custom-text-200">Show empty states</h4>
|
||||
<h4 className="text-custom-text-200">Show empty groups</h4>
|
||||
<div className="w-28">
|
||||
<ToggleSwitch
|
||||
value={displayFilters.show_empty_groups ?? true}
|
||||
|
||||
@@ -12,6 +12,7 @@ import stateService from "services/state.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
// components
|
||||
import {
|
||||
AllLists,
|
||||
@@ -86,6 +87,8 @@ export const AllViews: React.FC<Props> = ({
|
||||
|
||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||
|
||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug
|
||||
@@ -174,6 +177,8 @@ export const AllViews: React.FC<Props> = ({
|
||||
) : displayFilters?.layout === "spreadsheet" ? (
|
||||
<SpreadsheetView
|
||||
handleIssueAction={handleIssueAction}
|
||||
spreadsheetIssues={spreadsheetIssues}
|
||||
mutateIssues={mutateIssues}
|
||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
|
||||
@@ -76,7 +76,7 @@ export const AllBoards: React.FC<Props> = ({
|
||||
readOnly={disableUserActions}
|
||||
/>
|
||||
{groupedIssues ? (
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
|
||||
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8 bg-custom-background-90">
|
||||
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||
const currentState =
|
||||
displayFilters?.group_by === "state"
|
||||
|
||||
@@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -50,8 +50,6 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
@@ -61,6 +59,15 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: workspaceLabels } = useSWR(
|
||||
workspaceSlug && displayFilters?.group_by === "labels"
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
: null,
|
||||
workspaceSlug && displayFilters?.group_by === "labels"
|
||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
@@ -84,7 +91,10 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
title =
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
@@ -139,7 +149,9 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// components
|
||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
||||
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@@ -34,31 +34,39 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleBoard: React.FC<Props> = ({
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const SingleBoard: React.FC<Props> = (props) => {
|
||||
const {
|
||||
addIssueToGroup,
|
||||
currentState,
|
||||
groupTitle,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
dragDisabled,
|
||||
handleIssueAction,
|
||||
handleDraftIssueAction,
|
||||
handleTrashBox,
|
||||
openIssuesListModal,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
} = props;
|
||||
|
||||
// collapse/expand
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
|
||||
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
|
||||
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
||||
@@ -67,6 +75,24 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||
|
||||
const onCreateClick = () => {
|
||||
setIsInlineCreateIssueFormOpen(true);
|
||||
|
||||
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
|
||||
|
||||
// timeout is needed because the animation
|
||||
// takes time to complete & we can scroll only after that
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (boardListElement)
|
||||
boardListElement.scrollBy({
|
||||
top: boardListElement.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||
<BoardHeader
|
||||
@@ -115,6 +141,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
id={`board-list-${groupTitle}`}
|
||||
className={`pt-3 ${
|
||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||
} `}
|
||||
@@ -134,6 +161,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
type={type}
|
||||
index={index}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||
@@ -169,6 +197,19 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
>
|
||||
<>{provided.placeholder}</>
|
||||
</span>
|
||||
|
||||
<BoardInlineCreateIssueForm
|
||||
isOpen={isInlineCreateIssueFormOpen}
|
||||
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by! === "labels"
|
||||
? "labels_list"
|
||||
: displayFilters?.group_by!]:
|
||||
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{displayFilters?.group_by !== "created_by" && (
|
||||
<div>
|
||||
@@ -177,7 +218,11 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else onCreateClick();
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
@@ -197,7 +242,7 @@ export const SingleBoard: React.FC<Props> = ({
|
||||
position="left"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
||||
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
|
||||
@@ -13,19 +13,14 @@ import {
|
||||
} from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
@@ -44,7 +39,15 @@ import { LayerDiagonalIcon } from "components/icons";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
@@ -53,6 +56,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
index: number;
|
||||
editIssue: () => void;
|
||||
@@ -74,6 +78,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
index,
|
||||
editIssue,
|
||||
makeIssueCopy,
|
||||
@@ -101,7 +106,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||
|
||||
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||
|
||||
@@ -188,6 +193,86 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||
}, [snapshot, handleTrashBox]);
|
||||
@@ -343,13 +428,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-left break-words line-clamp-2"
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else openPeekOverview();
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -359,21 +443,20 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
projectId={projectId}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@@ -397,16 +480,24 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
customButton
|
||||
user={user}
|
||||
selfPositioned
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
||||
@@ -183,7 +183,10 @@ export const CalendarView: React.FC<Props> = ({
|
||||
{calendarIssues ? (
|
||||
<div className="h-full overflow-y-auto">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="h-full rounded-lg p-8 text-custom-text-200">
|
||||
<div
|
||||
id={`calendar-view-${cycleId ?? moduleId ?? viewId}`}
|
||||
className="h-full rounded-lg p-8 text-custom-text-200"
|
||||
>
|
||||
<CalendarHeader
|
||||
isMonthlyView={isMonthlyView}
|
||||
setIsMonthlyView={setIsMonthlyView}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
dependencies: any[];
|
||||
};
|
||||
|
||||
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>, deps: any[]) => {
|
||||
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
|
||||
|
||||
const router = useRouter();
|
||||
const { moduleId, cycleId, viewId } = router.query;
|
||||
|
||||
const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { right } = ref.current.getBoundingClientRect();
|
||||
|
||||
const width = right;
|
||||
|
||||
const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth;
|
||||
|
||||
if (width > innerWidth) setIsThereSpaceOnRight(false);
|
||||
else setIsThereSpaceOnRight(true);
|
||||
}, [ref, deps, container]);
|
||||
|
||||
return isThereSpaceOnRight;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||
{projectDetails?.identifier ?? "..."}
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||
const { isOpen, dependencies } = props;
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
|
||||
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
|
||||
>
|
||||
<InlineCreateIssueFormWrapper
|
||||
{...props}
|
||||
className="flex w-60 p-1 px-1.5 rounded items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-md transition-opacity"
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
</div>
|
||||
{/* Added to make any other element as outside click. This will make input also to be outside. */}
|
||||
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-beautiful-dnd
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
// component
|
||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import { SingleCalendarIssue } from "./single-issue";
|
||||
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||
// icons
|
||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||
// helper
|
||||
@@ -26,17 +30,14 @@ type Props = {
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SingleCalendarDate: React.FC<Props> = ({
|
||||
handleIssueAction,
|
||||
date,
|
||||
index,
|
||||
addIssueToDate,
|
||||
isMonthlyView,
|
||||
showWeekEnds,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { cycleId, moduleId } = router.query;
|
||||
|
||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const totalIssues = date.issues.length;
|
||||
|
||||
@@ -70,6 +71,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
user={user}
|
||||
@@ -78,6 +80,18 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
<CalendarInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen}
|
||||
dependencies={[showWeekEnds]}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
target_date: date.date,
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
}}
|
||||
/>
|
||||
|
||||
{totalIssues > 4 && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -93,7 +107,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
||||
>
|
||||
<button
|
||||
className="flex items-center justify-center gap-1 text-center"
|
||||
onClick={() => addIssueToDate(date.date)}
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||
Add issue
|
||||
|
||||
@@ -8,28 +8,23 @@ import { mutate } from "swr";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewLabelSelect,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// icons
|
||||
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helper
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// type
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
@@ -46,6 +41,7 @@ type Props = {
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
@@ -57,11 +53,12 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
provided,
|
||||
snapshot,
|
||||
issue,
|
||||
projectId,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@@ -153,6 +150,86 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const displayProperties = properties
|
||||
? Object.values(properties).some((value) => value === true)
|
||||
: false;
|
||||
@@ -225,22 +302,20 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{displayProperties && (
|
||||
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@@ -260,21 +335,25 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
|
||||
// react hook form
|
||||
import { useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
// headless ui
|
||||
import { Transition } from "@headlessui/react";
|
||||
|
||||
// services
|
||||
import modulesService from "services/modules.service";
|
||||
import issuesService from "services/issues.service";
|
||||
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useUser from "hooks/use-user";
|
||||
import useKeypress from "hooks/use-keypress";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
|
||||
// helpers
|
||||
import { getFetchKeysForIssueMutation } from "helpers/string.helper";
|
||||
|
||||
// fetch-keys
|
||||
import {
|
||||
USER_ISSUE,
|
||||
SUB_ISSUES,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||
} from "constants/fetch-keys";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
const defaultValues: Partial<IIssue> = {
|
||||
name: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const addIssueToCycle = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
cycleId: string,
|
||||
user: any,
|
||||
params: any
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await issuesService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
projectId.toString(),
|
||||
cycleId,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (cycleId) {
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
|
||||
mutate(CYCLE_DETAILS(cycleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addIssueToModule = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleId: string,
|
||||
user: any,
|
||||
params: any
|
||||
) => {
|
||||
await modulesService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
projectId.toString(),
|
||||
moduleId as string,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (moduleId) {
|
||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, onSuccess, prePopulatedData, children, className } = props;
|
||||
|
||||
const ref = useRef<HTMLFormElement>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { displayFilters, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
const { ...viewGanttParams } = params;
|
||||
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
const { params: ganttParams } = useGanttChartIssues(
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString()
|
||||
);
|
||||
|
||||
const method = useForm<IIssue>({ defaultValues });
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting },
|
||||
} = method;
|
||||
|
||||
useOutsideClickDetector(ref, handleClose);
|
||||
useKeypress("Escape", handleClose);
|
||||
|
||||
useEffect(() => {
|
||||
const values = getValues();
|
||||
|
||||
if (prePopulatedData) reset({ ...defaultValues, ...values, ...prePopulatedData });
|
||||
}, [reset, prePopulatedData, getValues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({ ...defaultValues });
|
||||
}, [isOpen, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!errors) return;
|
||||
|
||||
Object.keys(errors).forEach((key) => {
|
||||
const error = errors[key as keyof IIssue];
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
}, [errors, setToastAlert]);
|
||||
|
||||
const { calendarFetchKey, ganttFetchKey, spreadsheetFetchKey } = getFetchKeysForIssueMutation({
|
||||
cycleId: cycleId,
|
||||
moduleId: moduleId,
|
||||
viewId: viewId,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
calendarParams,
|
||||
spreadsheetParams,
|
||||
viewGanttParams,
|
||||
ganttParams,
|
||||
});
|
||||
|
||||
const onSubmitHandler = async (formData: IIssue) => {
|
||||
if (!workspaceSlug || !projectId || !user || isSubmitting) return;
|
||||
|
||||
reset({ ...defaultValues });
|
||||
|
||||
await (!isDraftIssues
|
||||
? issuesService.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
|
||||
: issuesService.createDraftIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
formData,
|
||||
user
|
||||
)
|
||||
)
|
||||
.then(async (res) => {
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||
if (formData.cycle && formData.cycle !== "")
|
||||
await addIssueToCycle(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
res.id,
|
||||
formData.cycle,
|
||||
user,
|
||||
params
|
||||
);
|
||||
if (formData.module && formData.module !== "")
|
||||
await addIssueToModule(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
res.id,
|
||||
formData.module,
|
||||
user,
|
||||
params
|
||||
);
|
||||
|
||||
if (isDraftIssues)
|
||||
await mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId.toString() ?? "", params));
|
||||
if (displayFilters.layout === "calendar") await mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart") await mutate(ganttFetchKey);
|
||||
if (displayFilters.layout === "spreadsheet") await mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) await mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
if (onSuccess) await onSuccess(res);
|
||||
|
||||
if (formData.assignees_list?.some((assignee) => assignee === user?.id))
|
||||
mutate(USER_ISSUE(workspaceSlug as string));
|
||||
|
||||
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
|
||||
})
|
||||
.catch((err) => {
|
||||
Object.keys(err || {}).forEach((key) => {
|
||||
const error = err?.[key];
|
||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: errorTitle || "Some error occurred. Please try again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={isOpen}
|
||||
enter="transition ease-in-out duration-200 transform"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="transition ease-in-out duration-200 transform"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<FormProvider {...method}>
|
||||
<form ref={ref} className={className} onSubmit={handleSubmit(onSubmitHandler)}>
|
||||
{children}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -87,8 +87,16 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
|
||||
useIssuesView();
|
||||
const {
|
||||
groupedByIssues,
|
||||
mutateIssues,
|
||||
displayFilters,
|
||||
filters,
|
||||
isEmpty,
|
||||
setFilters,
|
||||
params,
|
||||
setDisplayFilters,
|
||||
} = useIssuesView();
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
@@ -108,6 +116,17 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDraftIssues) return;
|
||||
|
||||
if (
|
||||
displayFilters.layout === "calendar" ||
|
||||
displayFilters.layout === "gantt_chart" ||
|
||||
displayFilters.layout === "spreadsheet"
|
||||
)
|
||||
setDisplayFilters({ layout: "list" });
|
||||
}, [isDraftIssues, displayFilters, setDisplayFilters]);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
@@ -462,6 +481,7 @@ export const IssuesView: React.FC<Props> = ({
|
||||
<CreateUpdateViewModal
|
||||
isOpen={createViewModal !== null}
|
||||
handleClose={() => setCreateViewModal(null)}
|
||||
viewType="project"
|
||||
preLoadedData={createViewModal}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useEffect } from "react";
|
||||
// react hook form
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
// hooks
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
|
||||
// components
|
||||
import { InlineCreateIssueFormWrapper } from "../inline-issue-create-wrapper";
|
||||
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||
prePopulatedData?: Partial<IIssue>;
|
||||
};
|
||||
|
||||
const InlineInput = () => {
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const { register, setFocus } = useFormContext();
|
||||
|
||||
useEffect(() => {
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||
{projectDetails?.identifier ?? "..."}
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder="Issue Title"
|
||||
{...register("name", {
|
||||
required: "Issue title is required.",
|
||||
})}
|
||||
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||
<>
|
||||
<InlineCreateIssueFormWrapper
|
||||
className="flex py-3 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-md"
|
||||
{...props}
|
||||
>
|
||||
<InlineInput />
|
||||
</InlineCreateIssueFormWrapper>
|
||||
{props.isOpen && (
|
||||
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
|
||||
Press {"'"}Enter{"'"} to add another issue
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -6,19 +6,13 @@ import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// icons
|
||||
@@ -40,8 +34,10 @@ import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
IUserProfileProjectSegregation,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
@@ -55,6 +51,7 @@ import {
|
||||
type Props = {
|
||||
type?: string;
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
groupTitle?: string;
|
||||
editIssue: () => void;
|
||||
index: number;
|
||||
@@ -73,6 +70,7 @@ type Props = {
|
||||
export const SingleListIssue: React.FC<Props> = ({
|
||||
type,
|
||||
issue,
|
||||
projectId,
|
||||
editIssue,
|
||||
index,
|
||||
makeIssueCopy,
|
||||
@@ -92,7 +90,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, userId } = router.query;
|
||||
const { workspaceSlug, cycleId, moduleId, userId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||
|
||||
@@ -181,6 +179,86 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const issuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||
: isDraftIssues
|
||||
@@ -290,21 +368,20 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@@ -323,14 +400,26 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={3}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
@@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
||||
// hooks
|
||||
import useProjects from "hooks/use-projects";
|
||||
// components
|
||||
import { SingleListIssue } from "components/core";
|
||||
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||
// ui
|
||||
import { Avatar, CustomMenu } from "components/ui";
|
||||
// icons
|
||||
@@ -31,7 +34,7 @@ import {
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||
// constants
|
||||
import { STATE_GROUP_COLORS } from "constants/state";
|
||||
|
||||
@@ -51,40 +54,65 @@ type Props = {
|
||||
viewProps: IIssueViewProps;
|
||||
};
|
||||
|
||||
export const SingleList: React.FC<Props> = ({
|
||||
currentState,
|
||||
groupTitle,
|
||||
addIssueToGroup,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
}) => {
|
||||
export const SingleList: React.FC<Props> = (props) => {
|
||||
const {
|
||||
currentState,
|
||||
groupTitle,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
handleDraftIssueAction,
|
||||
handleMyIssueOpen,
|
||||
addIssueToGroup,
|
||||
removeIssue,
|
||||
disableUserActions,
|
||||
disableAddIssueOption = false,
|
||||
user,
|
||||
userAuth,
|
||||
viewProps,
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||
|
||||
const isMyIssuesPage = router.pathname.split("/")[3] === "my-issues";
|
||||
const isProfileIssuesPage = router.pathname.split("/")[2] === "profile";
|
||||
const isDraftIssuesPage = router.pathname.split("/")[4] === "draft-issues";
|
||||
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
: null,
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: workspaceLabels } = useSWR(
|
||||
workspaceSlug && displayFilters?.group_by === "labels"
|
||||
? WORKSPACE_LABELS(workspaceSlug.toString())
|
||||
: null,
|
||||
workspaceSlug && displayFilters?.group_by === "labels"
|
||||
? () => issuesService.getWorkspaceLabels(workspaceSlug.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: members } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? PROJECT_MEMBERS(projectId as string)
|
||||
: null,
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
|
||||
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
@@ -99,7 +127,10 @@ export const SingleList: React.FC<Props> = ({
|
||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||
break;
|
||||
case "labels":
|
||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
||||
title =
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.name ?? "None";
|
||||
break;
|
||||
case "project":
|
||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||
@@ -153,7 +184,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
break;
|
||||
case "labels":
|
||||
const labelColor =
|
||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
||||
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||
(label) => label.id === groupTitle
|
||||
)?.color ?? "#000000";
|
||||
icon = (
|
||||
<span
|
||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||
@@ -207,7 +240,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={addIssueToGroup}
|
||||
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -224,7 +257,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
position="right"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
@@ -250,6 +285,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
key={issue.id}
|
||||
type={type}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
groupTitle={groupTitle}
|
||||
index={index}
|
||||
editIssue={() => handleIssueAction(issue, "edit")}
|
||||
@@ -284,6 +320,33 @@ export const SingleList: React.FC<Props> = ({
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||
)}
|
||||
|
||||
<ListInlineCreateIssueForm
|
||||
isOpen={isCreateIssueFormOpen && !disableAddIssueOption}
|
||||
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||
prePopulatedData={{
|
||||
...(cycleId && { cycle: cycleId.toString() }),
|
||||
...(moduleId && { module: moduleId.toString() }),
|
||||
[displayFilters?.group_by!]: groupTitle,
|
||||
}}
|
||||
/>
|
||||
|
||||
{!disableAddIssueOption && !isCreateIssueFormOpen && (
|
||||
<div className="w-full bg-custom-background-100 px-6 py-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||
addIssueToGroup();
|
||||
} else setIsCreateIssueFormOpen(true);
|
||||
}}
|
||||
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { MembersSelect } from "components/project";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const AssigneeColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
projectId={projectId}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-assignee-column";
|
||||
export * from "./assignee-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { AssigneeColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AssigneeColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetAssigneeColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
// helper
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const CreatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.created_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-created-on-column";
|
||||
export * from "./created-on-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { CreatedOnColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetCreatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreatedOnColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetCreatedOnColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewDueDateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const DueDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.due_date && (
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-due-date-column";
|
||||
export * from "./due-date-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { DueDateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DueDateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetDueDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewEstimateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const EstimateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.estimate && (
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-estimate-column";
|
||||
export * from "./estimate-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { EstimateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetEstimateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EstimateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetEstimateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,14 @@
|
||||
export * from "./assignee-column";
|
||||
export * from "./created-on-column";
|
||||
export * from "./due-date-column";
|
||||
export * from "./estimate-column";
|
||||
export * from "./issue-column";
|
||||
export * from "./label-column";
|
||||
export * from "./priority-column";
|
||||
export * from "./start-date-column";
|
||||
export * from "./state-column";
|
||||
export * from "./updated-on-column";
|
||||
export * from "./spreadsheet-view";
|
||||
export * from "./single-issue";
|
||||
export * from "./issue-column/issue-column";
|
||||
export * from "./spreadsheet-columns";
|
||||
export * from "./spreadsheet-issues";
|
||||
export * from "./issue-column/spreadsheet-issue-column";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-issue-column";
|
||||
export * from "./issue-column";
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import {
|
||||
EllipsisHorizontalIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { IIssue, Properties, UserAuth } from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
expanded: boolean;
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
properties: Properties;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
disableUserActions: boolean;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel: number;
|
||||
};
|
||||
|
||||
export const IssueColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
properties,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
setCurrentProjectId,
|
||||
disableUserActions,
|
||||
userAuth,
|
||||
nestingLevel,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const openPeekOverview = () => {
|
||||
const { query } = router;
|
||||
setCurrentProjectId(issue.project_detail.id);
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssue: issue.id },
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 54}px`;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<div className="group flex items-center w-[28rem] text-sm h-11 sticky top-0 bg-custom-background-100 truncate border-b border-r border-custom-border-200 ">
|
||||
<div
|
||||
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center"
|
||||
style={issue.parent ? { paddingLeft } : {}}
|
||||
>
|
||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||
{properties.key && (
|
||||
<span className="flex items-center justify-center font-medium opacity-100 group-hover:opacity-0">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
{!isNotAllowed && !disableUserActions && (
|
||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||
<Popover2
|
||||
isOpen={isOpen}
|
||||
canEscapeKeyClose
|
||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||
content={
|
||||
<div
|
||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleEditIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleDeleteIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleCopyText();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
|
||||
</Popover2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issue.sub_issues_count > 0 && (
|
||||
<div className="h-6 w-6 flex justify-center items-center">
|
||||
<button
|
||||
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
|
||||
onClick={() => handleToggleExpand(issue.id)}
|
||||
>
|
||||
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex items-center px-4 py-2.5 h-full truncate flex-grow">
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||
onClick={openPeekOverview}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+15
-19
@@ -1,36 +1,34 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { SingleSpreadsheetIssue } from "components/core";
|
||||
import { IssueColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
import { IIssue, Properties, UserAuth } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
index: number;
|
||||
projectId: string;
|
||||
expandedIssues: string[];
|
||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
properties: Properties;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
gridTemplateColumns: string;
|
||||
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel?: number;
|
||||
};
|
||||
|
||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
index,
|
||||
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
expandedIssues,
|
||||
setExpandedIssues,
|
||||
gridTemplateColumns,
|
||||
properties,
|
||||
handleIssueAction,
|
||||
setCurrentProjectId,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
nestingLevel = 0,
|
||||
}) => {
|
||||
@@ -49,21 +47,20 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.id, isExpanded);
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SingleSpreadsheetIssue
|
||||
<IssueColumn
|
||||
issue={issue}
|
||||
index={index}
|
||||
projectId={projectId}
|
||||
expanded={isExpanded}
|
||||
handleToggleExpand={handleToggleExpand}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel}
|
||||
/>
|
||||
@@ -73,17 +70,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetIssues
|
||||
<SpreadsheetIssuesColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
index={index}
|
||||
projectId={subIssue.project_detail.id}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
nestingLevel={nestingLevel + 1}
|
||||
/>
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-label-column";
|
||||
export * from "./label-column";
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LabelSelect } from "components/project";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const LabelColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
projectId={projectId}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LabelColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetLabelColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LabelColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetLabelColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-priority-column";
|
||||
export * from "./priority-column";
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { PrioritySelect } from "components/project";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, TIssuePriorities } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const PriorityColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.priority && (
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { PriorityColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PriorityColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetPriorityColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,380 +0,0 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
import {
|
||||
EllipsisHorizontalIcon,
|
||||
LinkIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// hooks
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// constant
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
index: number;
|
||||
expanded: boolean;
|
||||
handleToggleExpand: (issueId: string) => void;
|
||||
properties: Properties;
|
||||
handleEditIssue: (issue: IIssue) => void;
|
||||
handleDeleteIssue: (issue: IIssue) => void;
|
||||
gridTemplateColumns: string;
|
||||
disableUserActions: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
userAuth: UserAuth;
|
||||
nestingLevel: number;
|
||||
};
|
||||
|
||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
issue,
|
||||
index,
|
||||
expanded,
|
||||
handleToggleExpand,
|
||||
properties,
|
||||
handleEditIssue,
|
||||
handleDeleteIssue,
|
||||
gridTemplateColumns,
|
||||
disableUserActions,
|
||||
user,
|
||||
userAuth,
|
||||
nestingLevel,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
sub_issues: (prevData.sub_issues ?? []).map((i) => {
|
||||
if (i.id === issue.id) {
|
||||
return {
|
||||
...i,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
else
|
||||
mutate<IIssue[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((p) => {
|
||||
if (p.id === issue.id) {
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
} else {
|
||||
mutate(fetchKey);
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, moduleId, viewId, params, user]
|
||||
);
|
||||
|
||||
const openPeekOverview = () => {
|
||||
const { query } = router;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...query, peekIssue: issue.id },
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyText = () => {
|
||||
const originURL =
|
||||
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||
copyTextToClipboard(
|
||||
`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`
|
||||
).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 68}px`;
|
||||
|
||||
const tooltipPosition = index === 0 ? "bottom" : "top";
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
|
||||
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
|
||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
|
||||
{properties.key && (
|
||||
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
|
||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
{!isNotAllowed && !disableUserActions && (
|
||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||
<Popover2
|
||||
isOpen={isOpen}
|
||||
canEscapeKeyClose
|
||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||
content={
|
||||
<div
|
||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleEditIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleDeleteIssue(issue);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<span>Delete issue</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-custom-text-200 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
handleCopyText();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<span>Copy issue link</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
|
||||
</Popover2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issue.sub_issues_count > 0 && (
|
||||
<div className="h-6 w-6 flex justify-center items-center">
|
||||
<button
|
||||
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
|
||||
onClick={() => handleToggleExpand(issue.id)}
|
||||
>
|
||||
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||
onClick={openPeekOverview}
|
||||
>
|
||||
{issue.name}
|
||||
</button>
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.priority && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={1} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{properties.start_date && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{properties.due_date && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewDueDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.estimate && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewEstimateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.created_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.created_at)}
|
||||
</div>
|
||||
)}
|
||||
{properties.updated_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { KeyedMutator, mutate } from "swr";
|
||||
|
||||
// components
|
||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
||||
import { CustomMenu, Spinner } from "components/ui";
|
||||
import {
|
||||
SpreadsheetAssigneeColumn,
|
||||
SpreadsheetCreatedOnColumn,
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetIssuesColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
SpreadsheetStateColumn,
|
||||
SpreadsheetUpdatedOnColumn,
|
||||
} from "components/core";
|
||||
import { Spinner } from "components/ui";
|
||||
import { IssuePeekOverview } from "components/issues";
|
||||
// hooks
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
||||
// constants
|
||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, UserAuth } from "types";
|
||||
import useWorkspaceIssuesFilters from "hooks/use-worskpace-issue-filter";
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_DETAILS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
WORKSPACE_VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import projectIssuesServices from "services/issues.service";
|
||||
// icon
|
||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
spreadsheetIssues: IIssue[];
|
||||
mutateIssues: KeyedMutator<
|
||||
| IIssue[]
|
||||
| {
|
||||
[key: string]: IIssue[];
|
||||
}
|
||||
>;
|
||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||
openIssuesListModal?: (() => void) | null;
|
||||
disableUserActions: boolean;
|
||||
@@ -26,6 +55,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SpreadsheetView: React.FC<Props> = ({
|
||||
spreadsheetIssues,
|
||||
mutateIssues,
|
||||
handleIssueAction,
|
||||
openIssuesListModal,
|
||||
disableUserActions,
|
||||
@@ -33,113 +64,218 @@ export const SpreadsheetView: React.FC<Props> = ({
|
||||
userAuth,
|
||||
}) => {
|
||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||
|
||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||
|
||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, workspaceViewId } = router.query;
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
||||
...column,
|
||||
isActive: properties
|
||||
? column.propertyName === "labels"
|
||||
? properties[column.propertyName as keyof Properties]
|
||||
: column.propertyName === "title"
|
||||
? true
|
||||
: properties[column.propertyName as keyof Properties]
|
||||
: false,
|
||||
}));
|
||||
const workspaceIssuesPath = [
|
||||
{
|
||||
params: {
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/all-issues",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
assignees: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/assigned",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
created_by: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/created",
|
||||
},
|
||||
{
|
||||
params: {
|
||||
subscriber: user?.id ?? undefined,
|
||||
sub_issue: false,
|
||||
},
|
||||
path: "workspace-views/subscribed",
|
||||
},
|
||||
];
|
||||
|
||||
const gridTemplateColumns = columnData
|
||||
.filter((column) => column.isActive)
|
||||
.map((column) => column.colSize)
|
||||
.join(" ");
|
||||
const currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
||||
router.pathname.includes(path.path)
|
||||
);
|
||||
|
||||
const { params: workspaceViewParams } = useWorkspaceIssuesFilters(
|
||||
workspaceSlug?.toString(),
|
||||
workspaceViewId?.toString()
|
||||
);
|
||||
|
||||
const { params } = useSpreadsheetIssuesView();
|
||||
|
||||
const partialUpdateIssue = useCallback(
|
||||
(formData: Partial<IIssue>, issue: IIssue) => {
|
||||
if (!workspaceSlug || !issue) return;
|
||||
|
||||
const fetchKey = cycleId
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
|
||||
: moduleId
|
||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||
: viewId
|
||||
? VIEW_ISSUES(viewId.toString(), params)
|
||||
: workspaceViewId
|
||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), workspaceViewParams)
|
||||
: currentWorkspaceIssuePath
|
||||
? WORKSPACE_VIEW_ISSUES(workspaceSlug.toString(), currentWorkspaceIssuePath?.params)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(issue.project_detail.id, params);
|
||||
|
||||
if (issue.parent)
|
||||
mutate<ISubIssueResponse>(
|
||||
SUB_ISSUES(issue.parent.toString()),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
sub_issues: (prevData.sub_issues ?? []).map((i) => {
|
||||
if (i.id === issue.id) {
|
||||
return {
|
||||
...i,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return i;
|
||||
}),
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
else
|
||||
mutate<IIssue[]>(
|
||||
fetchKey,
|
||||
(prevData) =>
|
||||
(prevData ?? []).map((p) => {
|
||||
if (p.id === issue.id) {
|
||||
return {
|
||||
...p,
|
||||
...formData,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
projectIssuesServices
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
issue.project_detail.id,
|
||||
issue.id as string,
|
||||
formData,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (issue.parent) {
|
||||
mutate(SUB_ISSUES(issue.parent as string));
|
||||
} else {
|
||||
mutate(fetchKey);
|
||||
|
||||
if (cycleId) mutate(CYCLE_DETAILS(cycleId as string));
|
||||
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
[
|
||||
workspaceSlug,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
workspaceViewId,
|
||||
currentWorkspaceIssuePath,
|
||||
workspaceViewParams,
|
||||
params,
|
||||
user,
|
||||
]
|
||||
);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
|
||||
const renderColumn = (header: string, Component: React.ComponentType<any>) => (
|
||||
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm">
|
||||
<div className="flex items-center min-w-[9rem] px-4 py-2.5 text-sm font-medium z-[1] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||
{header}
|
||||
</div>
|
||||
<div className="h-full min-w-[9rem] w-full">
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<Component
|
||||
key={`${issue.id}_${index}`}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview
|
||||
handleMutation={() => mutateIssues()}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
projectId={currentProjectId ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
readOnly={disableUserActions}
|
||||
/>
|
||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
||||
</div>
|
||||
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
||||
{spreadsheetIssues ? (
|
||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<SpreadsheetIssues
|
||||
key={`${issue.id}_${index}`}
|
||||
index={index}
|
||||
issue={issue}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
user={user}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||
style={{ gridTemplateColumns }}
|
||||
>
|
||||
{type === "issue" ? (
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
) : (
|
||||
!disableUserActions && (
|
||||
<CustomMenu
|
||||
className="sticky left-0 z-[1]"
|
||||
customButton={
|
||||
<button
|
||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||
type="button"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Issue
|
||||
</button>
|
||||
}
|
||||
position="left"
|
||||
optionsClassName="left-5 !w-36"
|
||||
noBorder
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
>
|
||||
Create new
|
||||
</CustomMenu.MenuItem>
|
||||
{openIssuesListModal && (
|
||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||
Add an existing issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)
|
||||
)}
|
||||
<>
|
||||
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||
<div className="relative flex flex-col h-max w-full bg-custom-background-100 rounded-sm z-[2]">
|
||||
<div className="flex items-center text-sm font-medium z-[2] h-11 w-full sticky top-0 bg-custom-background-90 border border-l-0 border-custom-border-200">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-20 flex-shrink-0">
|
||||
ID
|
||||
</span>
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-grow">
|
||||
Issue
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
||||
<SpreadsheetIssuesColumn
|
||||
key={`${issue.id}_${index}`}
|
||||
issue={issue}
|
||||
projectId={issue.project_detail.id}
|
||||
expandedIssues={expandedIssues}
|
||||
setExpandedIssues={setExpandedIssues}
|
||||
setCurrentProjectId={setCurrentProjectId}
|
||||
properties={properties}
|
||||
handleIssueAction={handleIssueAction}
|
||||
disableUserActions={disableUserActions}
|
||||
userAuth={userAuth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderColumn("State", SpreadsheetStateColumn)}
|
||||
{renderColumn("Priority", SpreadsheetPriorityColumn)}
|
||||
{renderColumn("Assignees", SpreadsheetAssigneeColumn)}
|
||||
{renderColumn("Label", SpreadsheetLabelColumn)}
|
||||
{renderColumn("Start Date", SpreadsheetStartDateColumn)}
|
||||
{renderColumn("Due Date", SpreadsheetDueDateColumn)}
|
||||
{renderColumn("Estimate", SpreadsheetEstimateColumn)}
|
||||
{renderColumn("Created On", SpreadsheetCreatedOnColumn)}
|
||||
{renderColumn("Updated On", SpreadsheetUpdatedOnColumn)}
|
||||
</>
|
||||
) : (
|
||||
<Spinner />
|
||||
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-start-date-column";
|
||||
export * from "./start-date-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { StartDateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StartDateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStartDateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { ViewStartDateSelect } from "components/issues";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const StartDateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.due_date && (
|
||||
<ViewStartDateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-state-column";
|
||||
export * from "./state-column";
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { StateColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StateColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetStateColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { StateSelect } from "components/states";
|
||||
// services
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IState, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const StateColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.state && (
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
projectId={projectId}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./spreadsheet-updated-on-column";
|
||||
export * from "./updated-on-column";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { UpdatedOnColumn } from "components/core";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
expandedIssues: string[];
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetUpdatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
expandedIssues,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UpdatedOnColumn
|
||||
issue={issue}
|
||||
projectId={projectId}
|
||||
properties={properties}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
!isLoading &&
|
||||
subIssues &&
|
||||
subIssues.length > 0 &&
|
||||
subIssues.map((subIssue: IIssue) => (
|
||||
<SpreadsheetUpdatedOnColumn
|
||||
key={subIssue.id}
|
||||
issue={subIssue}
|
||||
projectId={subIssue.project_detail.id}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
expandedIssues={expandedIssues}
|
||||
properties={properties}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, Properties } from "types";
|
||||
// helper
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
projectId: string;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
properties: Properties;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const UpdatedOnColumn: React.FC<Props> = ({
|
||||
issue,
|
||||
projectId,
|
||||
partialUpdateIssue,
|
||||
properties,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => (
|
||||
<div className="flex items-center text-sm h-11 w-full bg-custom-background-100">
|
||||
<span className="flex items-center px-4 py-2.5 h-full w-full flex-shrink-0 border-r border-b border-custom-border-200">
|
||||
{properties.updated_on && (
|
||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
{renderLongDetailDateFormat(issue.updated_at)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -127,7 +127,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user