forked from github/plane
fix: merge conflicts
This commit is contained in:
commit
4f33c4bea3
6
.github/workflows/Update_Docker_Images.yml
vendored
6
.github/workflows/Update_Docker_Images.yml
vendored
@ -39,10 +39,10 @@ jobs:
|
|||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: metaDeploy
|
id: metaSpace
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
with:
|
with:
|
||||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
file: ./space/Dockerfile.space
|
file: ./space/Dockerfile.space
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
tags: ${{ steps.metaSpace.outputs.tags }}
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
@ -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:
|
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
|
- 3rd-party libraries being used and their versions
|
||||||
- a use-case that fails
|
- 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.
|
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
|
### Requirements
|
||||||
|
|
||||||
- Node.js version v16.18.0
|
- Node.js version v16.18.0
|
||||||
- Python version 3.8+
|
- Python version 3.8+
|
||||||
- Postgres version v14
|
- Postgres version v14
|
||||||
- Redis version v6.2.7
|
- Redis version v6.2.7
|
||||||
|
|
||||||
### Setup the project
|
### 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
|
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?
|
## 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.
|
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:
|
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).
|
- 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.
|
- 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
|
## 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
|
## Ways to contribute
|
||||||
|
|
||||||
- Try Plane Cloud and the self hosting platform and give feedback
|
- Try Plane Cloud and the self hosting platform and give feedback
|
||||||
- Add new integrations
|
- 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)
|
- 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
|
- Share your thoughts and suggestions with us
|
||||||
- Help create tutorials and blog posts
|
- Help create tutorials and blog posts
|
||||||
- Request a feature by submitting a proposal
|
- Request a feature by submitting a proposal
|
||||||
- Report a bug
|
- Report a bug
|
||||||
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
|
||||||
|
@ -58,8 +58,17 @@ class WorkspaceEntityPermission(BasePermission):
|
|||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
|
if request.method in SAFE_METHODS:
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
).exists()
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role__in=[Owner, Admin],
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ class CycleSerializer(BaseSerializer):
|
|||||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||||
backlog_issues = serializers.IntegerField(read_only=True)
|
backlog_issues = serializers.IntegerField(read_only=True)
|
||||||
assignees = serializers.SerializerMethodField(read_only=True)
|
assignees = serializers.SerializerMethodField(read_only=True)
|
||||||
labels = serializers.SerializerMethodField(read_only=True)
|
|
||||||
total_estimates = serializers.IntegerField(read_only=True)
|
total_estimates = serializers.IntegerField(read_only=True)
|
||||||
completed_estimates = serializers.IntegerField(read_only=True)
|
completed_estimates = serializers.IntegerField(read_only=True)
|
||||||
started_estimates = serializers.IntegerField(read_only=True)
|
started_estimates = serializers.IntegerField(read_only=True)
|
||||||
@ -50,11 +49,10 @@ class CycleSerializer(BaseSerializer):
|
|||||||
members = [
|
members = [
|
||||||
{
|
{
|
||||||
"avatar": assignee.avatar,
|
"avatar": assignee.avatar,
|
||||||
"first_name": assignee.first_name,
|
|
||||||
"display_name": assignee.display_name,
|
"display_name": assignee.display_name,
|
||||||
"id": assignee.id,
|
"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()
|
for assignee in issue_cycle.issue.assignees.all()
|
||||||
]
|
]
|
||||||
# Use a set comprehension to return only the unique objects
|
# 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]
|
unique_list = [dict(item) for item in unique_objects]
|
||||||
|
|
||||||
return unique_list
|
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:
|
class Meta:
|
||||||
model = Cycle
|
model = Cycle
|
||||||
|
@ -80,7 +80,7 @@ class CycleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -102,48 +102,84 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.select_related("owned_by")
|
.select_related("owned_by")
|
||||||
.annotate(is_favorite=Exists(subquery))
|
.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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_cycle__issue__state__group",
|
"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(total_estimates=Sum("issue_cycle__issue__estimate_point"))
|
||||||
.annotate(
|
.annotate(
|
||||||
completed_estimates=Sum(
|
completed_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"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(
|
.annotate(
|
||||||
started_estimates=Sum(
|
started_estimates=Sum(
|
||||||
"issue_cycle__issue__estimate_point",
|
"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(
|
.prefetch_related(
|
||||||
@ -196,17 +232,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("display_name", "assignee_id", "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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("display_name")
|
||||||
@ -222,17 +271,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -385,17 +447,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.values(
|
.values(
|
||||||
"first_name", "last_name", "assignee_id", "avatar", "display_name"
|
"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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("first_name", "last_name")
|
||||||
@ -412,17 +487,30 @@ class CycleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -488,7 +576,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -664,7 +752,7 @@ class CycleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return all Cycle Issues
|
# Return all Cycle Issues
|
||||||
|
@ -384,7 +384,7 @@ class BulkImportIssuesEndpoint(BaseAPIView):
|
|||||||
sort_order=largest_sort_order,
|
sort_order=largest_sort_order,
|
||||||
start_date=issue_data.get("start_date", None),
|
start_date=issue_data.get("start_date", None),
|
||||||
target_date=issue_data.get("target_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,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -173,12 +173,12 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# 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",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -213,7 +213,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -278,7 +278,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -370,6 +370,11 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# Check the issue status
|
||||||
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
|
# Delete the issue also
|
||||||
|
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
except InboxIssue.DoesNotExist:
|
except InboxIssue.DoesNotExist:
|
||||||
@ -480,12 +485,12 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check for valid priority
|
# 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",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"urgent",
|
"urgent",
|
||||||
None,
|
"none",
|
||||||
]:
|
]:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -520,7 +525,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -585,7 +590,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -24,7 +24,6 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.conf import settings
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
@ -58,7 +57,6 @@ from plane.api.serializers import (
|
|||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
WorkspaceEntityPermission,
|
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
@ -130,7 +128,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -151,7 +149,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -318,7 +316,7 @@ class IssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -330,7 +328,12 @@ class IssueViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
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
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
@ -572,7 +575,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -591,7 +594,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -613,7 +616,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(current_instance).data,
|
IssueCommentSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -709,10 +712,18 @@ class LabelViewSet(BaseViewSet):
|
|||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def create(self, request, slug, project_id):
|
||||||
serializer.save(
|
try:
|
||||||
project_id=self.kwargs.get("project_id"),
|
serializer = LabelSerializer(data=request.data)
|
||||||
)
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response({"error": "Label with the same name already exists in the project"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
@ -897,7 +908,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id")),
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
project_id=str(self.kwargs.get("project_id")),
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
@ -916,7 +927,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
return super().perform_update(serializer)
|
||||||
@ -938,7 +949,7 @@ class IssueLinkViewSet(BaseViewSet):
|
|||||||
IssueLinkSerializer(current_instance).data,
|
IssueLinkSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -1017,7 +1028,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
serializer.data,
|
serializer.data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1040,7 +1051,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1243,7 +1254,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||||
@ -1448,7 +1459,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
@ -1472,7 +1483,7 @@ class IssueReactionViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1521,7 +1532,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
@ -1546,7 +1557,7 @@ class CommentReactionViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1643,7 +1654,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -1693,7 +1704,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1727,7 +1738,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
IssueCommentSerializer(comment).data,
|
IssueCommentSerializer(comment).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment.delete()
|
comment.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1802,7 +1813,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1847,7 +1858,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_reaction.id),
|
"identifier": str(issue_reaction.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_reaction.delete()
|
issue_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -1921,7 +1932,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
issue_id=None,
|
issue_id=None,
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -1973,7 +1984,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
"comment_id": str(comment_id),
|
"comment_id": str(comment_id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
comment_reaction.delete()
|
comment_reaction.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2037,7 +2048,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("issue_id", None)),
|
issue_id=str(self.kwargs.get("issue_id", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
serializer = IssueVoteSerializer(issue_vote)
|
serializer = IssueVoteSerializer(issue_vote)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -2072,7 +2083,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
"identifier": str(issue_vote.id),
|
"identifier": str(issue_vote.id),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
issue_vote.delete()
|
issue_vote.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -2106,7 +2117,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
IssueRelationSerializer(current_instance).data,
|
IssueRelationSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -2140,7 +2151,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue_id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
if relation == "blocking":
|
if relation == "blocking":
|
||||||
@ -2395,27 +2406,6 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
serializer_class = IssueFlatSerializer
|
serializer_class = IssueFlatSerializer
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
|
||||||
current_instance = (
|
|
||||||
self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first()
|
|
||||||
)
|
|
||||||
if current_instance is not None:
|
|
||||||
issue_activity.delay(
|
|
||||||
type="issue_draft.activity.updated",
|
|
||||||
requested_data=requested_data,
|
|
||||||
actor_id=str(self.request.user.id),
|
|
||||||
issue_id=str(self.kwargs.get("pk", None)),
|
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
|
||||||
current_instance=json.dumps(
|
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
|
||||||
),
|
|
||||||
epoch = int(timezone.now().timestamp())
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().perform_update(serializer)
|
|
||||||
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
@ -2434,6 +2424,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||||
),
|
),
|
||||||
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -2597,7 +2588,7 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
issue_id=str(serializer.data.get("id", None)),
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@ -2608,6 +2599,47 @@ class IssueDraftViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
try:
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
issue, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
if(request.data.get("is_draft") is not None and not request.data.get("is_draft")):
|
||||||
|
serializer.save(created_at=timezone.now(), updated_at=timezone.now())
|
||||||
|
else:
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.updated",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(issue).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)
|
||||||
|
except Issue.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue does not exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
return Response(
|
||||||
|
{"error": "Something went wrong please try again later"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, pk=None):
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
try:
|
try:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
|
@ -40,6 +40,7 @@ from plane.utils.grouper import group_results
|
|||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(BaseViewSet):
|
class ModuleViewSet(BaseViewSet):
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
@ -78,35 +79,63 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
queryset=ModuleLink.objects.select_related("module", "created_by"),
|
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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
cancelled_issues=Count(
|
cancelled_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
started_issues=Count(
|
started_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
unstarted_issues=Count(
|
unstarted_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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(
|
.annotate(
|
||||||
backlog_issues=Count(
|
backlog_issues=Count(
|
||||||
"issue_module__issue__state__group",
|
"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")
|
.order_by(order_by, "name")
|
||||||
@ -130,7 +159,7 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
@ -179,18 +208,36 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(assignee_id=F("assignees__id"))
|
.annotate(assignee_id=F("assignees__id"))
|
||||||
.annotate(display_name=F("assignees__display_name"))
|
.annotate(display_name=F("assignees__display_name"))
|
||||||
.annotate(avatar=F("assignees__avatar"))
|
.annotate(avatar=F("assignees__avatar"))
|
||||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
.values(
|
||||||
.annotate(total_issues=Count("assignee_id"))
|
"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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"assignee_id",
|
"assignee_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"assignee_id",
|
"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")
|
.order_by("first_name", "last_name")
|
||||||
@ -206,17 +253,33 @@ class ModuleViewSet(BaseViewSet):
|
|||||||
.annotate(color=F("labels__color"))
|
.annotate(color=F("labels__color"))
|
||||||
.annotate(label_id=F("labels__id"))
|
.annotate(label_id=F("labels__id"))
|
||||||
.values("label_name", "color", "label_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(
|
.annotate(
|
||||||
completed_issues=Count(
|
completed_issues=Count(
|
||||||
"label_id",
|
"label_id",
|
||||||
filter=Q(completed_at__isnull=False),
|
filter=Q(
|
||||||
|
completed_at__isnull=False,
|
||||||
|
archived_at__isnull=True,
|
||||||
|
is_draft=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
pending_issues=Count(
|
pending_issues=Count(
|
||||||
"label_id",
|
"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")
|
.order_by("label_name")
|
||||||
@ -279,7 +342,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(self.kwargs.get("pk", None)),
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
return super().perform_destroy(instance)
|
return super().perform_destroy(instance)
|
||||||
|
|
||||||
@ -447,7 +510,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@ -494,7 +557,6 @@ class ModuleLinkViewSet(BaseViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ModuleFavoriteViewSet(BaseViewSet):
|
class ModuleFavoriteViewSet(BaseViewSet):
|
||||||
|
|
||||||
serializer_class = ModuleFavoriteSerializer
|
serializer_class = ModuleFavoriteSerializer
|
||||||
model = ModuleFavorite
|
model = ModuleFavorite
|
||||||
|
|
||||||
|
@ -1094,7 +1094,7 @@ class ProjectMemberEndpoint(BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
member__is_bot=False,
|
member__is_bot=False,
|
||||||
).select_related("project", "member")
|
).select_related("project", "member", "workspace")
|
||||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -61,7 +61,7 @@ class GlobalViewViewSet(BaseViewSet):
|
|||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.select_related("workspace")
|
.select_related("workspace")
|
||||||
.order_by("-created_at")
|
.order_by(self.request.GET.get("order_by", "-created_at"))
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1239,13 +1239,21 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
.annotate(
|
.annotate(
|
||||||
created_issues=Count(
|
created_issues=Count(
|
||||||
"project_issue",
|
"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(
|
.annotate(
|
||||||
assigned_issues=Count(
|
assigned_issues=Count(
|
||||||
"project_issue",
|
"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(
|
.annotate(
|
||||||
@ -1254,6 +1262,8 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
filter=Q(
|
filter=Q(
|
||||||
project_issue__completed_at__isnull=False,
|
project_issue__completed_at__isnull=False,
|
||||||
project_issue__assignees__in=[user_id],
|
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",
|
"started",
|
||||||
],
|
],
|
||||||
project_issue__assignees__in=[user_id],
|
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):
|
def get(self, request, slug, user_id):
|
||||||
try:
|
try:
|
||||||
filters = issue_filters(request.query_params, "GET")
|
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")
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
issue_queryset = (
|
issue_queryset = (
|
||||||
Issue.issue_objects.filter(
|
Issue.issue_objects.filter(
|
||||||
|
@ -32,7 +32,7 @@ def delete_old_s3_link():
|
|||||||
else:
|
else:
|
||||||
s3 = boto3.client(
|
s3 = boto3.client(
|
||||||
"s3",
|
"s3",
|
||||||
region_name="ap-south-1",
|
region_name=settings.AWS_REGION,
|
||||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
config=Config(signature_version="s3v4"),
|
config=Config(signature_version="s3v4"),
|
||||||
|
@ -121,36 +121,20 @@ def track_priority(
|
|||||||
epoch
|
epoch
|
||||||
):
|
):
|
||||||
if current_instance.get("priority") != requested_data.get("priority"):
|
if current_instance.get("priority") != requested_data.get("priority"):
|
||||||
if requested_data.get("priority") == None:
|
issue_activities.append(
|
||||||
issue_activities.append(
|
IssueActivity(
|
||||||
IssueActivity(
|
issue_id=issue_id,
|
||||||
issue_id=issue_id,
|
actor=actor,
|
||||||
actor=actor,
|
verb="updated",
|
||||||
verb="updated",
|
old_value=current_instance.get("priority"),
|
||||||
old_value=current_instance.get("priority"),
|
new_value=requested_data.get("priority"),
|
||||||
new_value=None,
|
field="priority",
|
||||||
field="priority",
|
project=project,
|
||||||
project=project,
|
workspace=project.workspace,
|
||||||
workspace=project.workspace,
|
comment=f"updated the priority to {requested_data.get('priority')}",
|
||||||
comment=f"updated the priority to None",
|
epoch=epoch,
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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')}",
|
|
||||||
epoch=epoch,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Track chnages in state of the issue
|
# Track chnages in state of the issue
|
||||||
@ -1405,7 +1389,7 @@ def issue_activity(
|
|||||||
):
|
):
|
||||||
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
issue_subscribers = issue_subscribers + [issue.created_by_id]
|
||||||
|
|
||||||
for subscriber in issue_subscribers:
|
for subscriber in list(set(issue_subscribers)):
|
||||||
for issue_activity in issue_activities_created:
|
for issue_activity in issue_activities_created:
|
||||||
bulk_notifications.append(
|
bulk_notifications.append(
|
||||||
Notification(
|
Notification(
|
||||||
|
@ -58,28 +58,31 @@ def archive_old_issues():
|
|||||||
|
|
||||||
# Check if Issues
|
# Check if Issues
|
||||||
if issues:
|
if issues:
|
||||||
|
# Set the archive time to current time
|
||||||
|
archive_at = timezone.now()
|
||||||
|
|
||||||
issues_to_update = []
|
issues_to_update = []
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
issue.archived_at = timezone.now()
|
issue.archived_at = archive_at
|
||||||
issues_to_update.append(issue)
|
issues_to_update.append(issue)
|
||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
if issues_to_update:
|
||||||
updated_issues = Issue.objects.bulk_update(
|
Issue.objects.bulk_update(
|
||||||
issues_to_update, ["archived_at"], batch_size=100
|
issues_to_update, ["archived_at"], batch_size=100
|
||||||
)
|
)
|
||||||
[
|
[
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
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),
|
actor_id=str(project.created_by_id),
|
||||||
issue_id=issue.id,
|
issue_id=issue.id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -139,7 +142,7 @@ def close_old_issues():
|
|||||||
|
|
||||||
# Bulk Update the issues and log the activity
|
# Bulk Update the issues and log the activity
|
||||||
if issues_to_update:
|
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(
|
issue_activity.delay(
|
||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
@ -149,9 +152,9 @@ def close_old_issues():
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
subscriber=False,
|
subscriber=False,
|
||||||
epoch = int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp())
|
||||||
)
|
)
|
||||||
for issue in updated_issues
|
for issue in issues_to_update
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -33,9 +33,8 @@ def create_issue_relation(apps, schema_editor):
|
|||||||
def update_issue_priority_choice(apps, schema_editor):
|
def update_issue_priority_choice(apps, schema_editor):
|
||||||
IssueModel = apps.get_model("db", "Issue")
|
IssueModel = apps.get_model("db", "Issue")
|
||||||
updated_issues = []
|
updated_issues = []
|
||||||
for obj in IssueModel.objects.all():
|
for obj in IssueModel.objects.filter(priority=None):
|
||||||
if obj.priority is None:
|
obj.priority = "none"
|
||||||
obj.priority = "none"
|
|
||||||
updated_issues.append(obj)
|
updated_issues.append(obj)
|
||||||
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
|
||||||
|
|
||||||
|
@ -26,19 +26,19 @@ def workspace_member_props(old_props):
|
|||||||
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
"calendar_date_range": old_props.get("calendarDateRange", ""),
|
||||||
},
|
},
|
||||||
"display_properties": {
|
"display_properties": {
|
||||||
"assignee": old_props.get("properties", {}).get("assignee",None),
|
"assignee": old_props.get("properties", {}).get("assignee", True),
|
||||||
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
|
"attachment_count": old_props.get("properties", {}).get("attachment_count", True),
|
||||||
"created_on": old_props.get("properties", {}).get("created_on", None),
|
"created_on": old_props.get("properties", {}).get("created_on", True),
|
||||||
"due_date": old_props.get("properties", {}).get("due_date", None),
|
"due_date": old_props.get("properties", {}).get("due_date", True),
|
||||||
"estimate": old_props.get("properties", {}).get("estimate", None),
|
"estimate": old_props.get("properties", {}).get("estimate", True),
|
||||||
"key": old_props.get("properties", {}).get("key", None),
|
"key": old_props.get("properties", {}).get("key", True),
|
||||||
"labels": old_props.get("properties", {}).get("labels", None),
|
"labels": old_props.get("properties", {}).get("labels", True),
|
||||||
"link": old_props.get("properties", {}).get("link", None),
|
"link": old_props.get("properties", {}).get("link", True),
|
||||||
"priority": old_props.get("properties", {}).get("priority", None),
|
"priority": old_props.get("properties", {}).get("priority", True),
|
||||||
"start_date": old_props.get("properties", {}).get("start_date", None),
|
"start_date": old_props.get("properties", {}).get("start_date", True),
|
||||||
"state": old_props.get("properties", {}).get("state", None),
|
"state": old_props.get("properties", {}).get("state", True),
|
||||||
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
|
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", True),
|
||||||
"updated_on": old_props.get("properties", {}).get("updated_on", None),
|
"updated_on": old_props.get("properties", {}).get("updated_on", True),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return new_props
|
return new_props
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('db', '0044_auto_20230913_0709'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(update_issue_activity),
|
|
||||||
]
|
|
@ -1,28 +1,47 @@
|
|||||||
# Generated by Django 4.2.3 on 2023-09-19 14:21
|
# Generated by Django 4.2.5 on 2023-09-29 10:14
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import plane.db.models.workspace
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def update_epoch(apps, schema_editor):
|
def update_issue_activity_priority(apps, schema_editor):
|
||||||
IssueActivity = apps.get_model('db', 'IssueActivity')
|
IssueActivity = apps.get_model("db", "IssueActivity")
|
||||||
updated_issue_activity = []
|
updated_issue_activity = []
|
||||||
for obj in IssueActivity.objects.all():
|
for obj in IssueActivity.objects.filter(field="priority"):
|
||||||
obj.epoch = int(obj.created_at.timestamp())
|
# 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)
|
updated_issue_activity.append(obj)
|
||||||
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
IssueActivity.objects.bulk_update(
|
||||||
|
updated_issue_activity,
|
||||||
|
["new_value", "old_value"],
|
||||||
|
batch_size=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('db', '0045_auto_20230915_0655'),
|
('db', '0044_auto_20230913_0709'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='GlobalView',
|
name='GlobalView',
|
||||||
fields=[
|
fields=[
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
@ -33,6 +52,7 @@ class Migration(migrations.Migration):
|
|||||||
('query', models.JSONField(verbose_name='View Query')),
|
('query', models.JSONField(verbose_name='View Query')),
|
||||||
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||||
('query_data', models.JSONField(default=dict)),
|
('query_data', models.JSONField(default=dict)),
|
||||||
|
('sort_order', models.FloatField(default=65535)),
|
||||||
('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')),
|
('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')),
|
('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')),
|
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||||
@ -44,10 +64,16 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ('-created_at',),
|
'ordering': ('-created_at',),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='issue_props',
|
||||||
|
field=models.JSONField(default=plane.db.models.workspace.get_issue_props),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='issueactivity',
|
model_name='issueactivity',
|
||||||
name='epoch',
|
name='epoch',
|
||||||
field=models.FloatField(null=True),
|
field=models.FloatField(null=True),
|
||||||
),
|
),
|
||||||
migrations.RunPython(update_epoch),
|
migrations.RunPython(update_issue_activity_priority),
|
||||||
|
migrations.RunPython(update_issue_activity_blocked),
|
||||||
]
|
]
|
@ -17,12 +17,23 @@ class GlobalView(BaseModel):
|
|||||||
default=1, choices=((0, "Private"), (1, "Public"))
|
default=1, choices=((0, "Private"), (1, "Public"))
|
||||||
)
|
)
|
||||||
query_data = models.JSONField(default=dict)
|
query_data = models.JSONField(default=dict)
|
||||||
|
sort_order = models.FloatField(default=65535)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Global View"
|
verbose_name = "Global View"
|
||||||
verbose_name_plural = "Global Views"
|
verbose_name_plural = "Global Views"
|
||||||
db_table = "global_views"
|
db_table = "global_views"
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self._state.adding:
|
||||||
|
largest_sort_order = GlobalView.objects.filter(
|
||||||
|
workspace=self.workspace
|
||||||
|
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||||
|
if largest_sort_order is not None:
|
||||||
|
self.sort_order = largest_sort_order + 10000
|
||||||
|
|
||||||
|
super(GlobalView, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return name of the View"""
|
"""Return name of the View"""
|
||||||
|
@ -29,7 +29,7 @@ def get_default_props():
|
|||||||
},
|
},
|
||||||
"display_filters": {
|
"display_filters": {
|
||||||
"group_by": None,
|
"group_by": None,
|
||||||
"order_by": '-created_at',
|
"order_by": "-created_at",
|
||||||
"type": None,
|
"type": None,
|
||||||
"sub_issue": True,
|
"sub_issue": True,
|
||||||
"show_empty_groups": True,
|
"show_empty_groups": True,
|
||||||
@ -54,6 +54,15 @@ def get_default_props():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_issue_props():
|
||||||
|
return {
|
||||||
|
"subscribed": True,
|
||||||
|
"assigned": True,
|
||||||
|
"created": True,
|
||||||
|
"all_issues": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Workspace(BaseModel):
|
class Workspace(BaseModel):
|
||||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||||
@ -89,6 +98,7 @@ class WorkspaceMember(BaseModel):
|
|||||||
company_role = models.TextField(null=True, blank=True)
|
company_role = models.TextField(null=True, blank=True)
|
||||||
view_props = models.JSONField(default=get_default_props)
|
view_props = models.JSONField(default=get_default_props)
|
||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
|
issue_props = models.JSONField(default=get_issue_props)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["workspace", "member"]
|
unique_together = ["workspace", "member"]
|
||||||
|
@ -74,10 +74,10 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
|||||||
|
|
||||||
sorted_data = grouped_data
|
sorted_data = grouped_data
|
||||||
if temp_axis == "priority":
|
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}
|
sorted_data = {key: grouped_data[key] for key in order if key in grouped_data}
|
||||||
else:
|
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
|
return sorted_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,9 +40,6 @@ def filter_priority(params, filter, method):
|
|||||||
priorities = params.get("priority").split(",")
|
priorities = params.get("priority").split(",")
|
||||||
if len(priorities) and "" not in priorities:
|
if len(priorities) and "" not in priorities:
|
||||||
filter["priority__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
|
return filter
|
||||||
|
|
||||||
|
|
||||||
@ -166,17 +163,17 @@ def filter_target_date(params, filter, method):
|
|||||||
for query in target_dates:
|
for query in target_dates:
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
filter["target_date__gte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
filter["target_date__lte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
if params.get("target_date", None) and len(params.get("target_date")):
|
if params.get("target_date", None) and len(params.get("target_date")):
|
||||||
for query in params.get("target_date"):
|
for query in params.get("target_date"):
|
||||||
target_date_query = query.split(";")
|
target_date_query = query.split(";")
|
||||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||||
filter["target_date__gt"] = target_date_query[0]
|
filter["target_date__gte"] = target_date_query[0]
|
||||||
else:
|
else:
|
||||||
filter["target_date__lt"] = target_date_query[0]
|
filter["target_date__lte"] = target_date_query[0]
|
||||||
|
|
||||||
return filter
|
return filter
|
||||||
|
|
||||||
|
@ -1,113 +1,61 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
x-api-and-worker-env:
|
|
||||||
&api-and-worker-env
|
|
||||||
DEBUG: ${DEBUG}
|
|
||||||
SENTRY_DSN: ${SENTRY_DSN}
|
|
||||||
DJANGO_SETTINGS_MODULE: plane.settings.selfhosted
|
|
||||||
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:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
image: makeplane/plane-frontend:latest
|
image: makeplane/plane-frontend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./web/.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"
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
image: makeplane/plane-deploy:latest
|
image: makeplane/plane-space:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./space/.env
|
||||||
environment:
|
|
||||||
NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL}
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/takeoff
|
command: ./bin/takeoff
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/worker
|
command: ./bin/worker
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
image: makeplane/plane-backend:latest
|
image: makeplane/plane-backend:latest
|
||||||
restart: always
|
restart: always
|
||||||
command: ./bin/beat
|
command: ./bin/beat
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- ./apiserver/.env
|
||||||
environment:
|
|
||||||
<<: *api-and-worker-env
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -157,8 +105,8 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
image: makeplane/plane-proxy:latest
|
image: makeplane/plane-proxy:latest
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT}:80
|
- ${NGINX_PORT}:80
|
||||||
@ -168,8 +116,9 @@ services:
|
|||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
plane-web:
|
web:
|
||||||
container_name: planefrontend
|
container_name: web
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./web/Dockerfile.web
|
dockerfile: ./web/Dockerfile.web
|
||||||
@ -11,11 +11,11 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh web/server.js web
|
command: /usr/local/bin/start.sh web/server.js web
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
|
|
||||||
plane-deploy:
|
space:
|
||||||
container_name: planedeploy
|
container_name: space
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: ./space/Dockerfile.space
|
dockerfile: ./space/Dockerfile.space
|
||||||
@ -24,12 +24,12 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
command: /usr/local/bin/start.sh space/server.js space
|
command: /usr/local/bin/start.sh space/server.js space
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-worker
|
- worker
|
||||||
- plane-web
|
- web
|
||||||
|
|
||||||
plane-api:
|
api:
|
||||||
container_name: planebackend
|
container_name: api
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -43,8 +43,8 @@ services:
|
|||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-worker:
|
worker:
|
||||||
container_name: planebgworker
|
container_name: bgworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -55,12 +55,12 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./apiserver/.env
|
- ./apiserver/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
plane-beat-worker:
|
beat-worker:
|
||||||
container_name: planebeatworker
|
container_name: beatworker
|
||||||
build:
|
build:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
@ -71,7 +71,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- ./apiserver/.env
|
- ./apiserver/.env
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-api
|
- api
|
||||||
- plane-db
|
- plane-db
|
||||||
- plane-redis
|
- plane-redis
|
||||||
|
|
||||||
@ -118,8 +118,8 @@ services:
|
|||||||
- plane-minio
|
- plane-minio
|
||||||
|
|
||||||
# Comment this if you already have a reverse proxy running
|
# Comment this if you already have a reverse proxy running
|
||||||
plane-proxy:
|
proxy:
|
||||||
container_name: planeproxy
|
container_name: proxy
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -130,8 +130,9 @@ services:
|
|||||||
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||||
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||||
depends_on:
|
depends_on:
|
||||||
- plane-web
|
- web
|
||||||
- plane-api
|
- api
|
||||||
|
- space
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
@ -1,29 +1,36 @@
|
|||||||
events { }
|
events {
|
||||||
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /www/data/;
|
root /www/data/;
|
||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
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 / {
|
location / {
|
||||||
proxy_pass http://planefrontend:3000/;
|
proxy_pass http://web:3000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://planebackend:8000/api/;
|
proxy_pass http://api:8000/api/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /spaces/ {
|
location /spaces/ {
|
||||||
proxy_pass http://planedeploy:3000/spaces/;
|
rewrite ^/spaces/?$ /spaces/login break;
|
||||||
|
proxy_pass http://space:3000/spaces/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /${BUCKET_NAME}/ {
|
location /${BUCKET_NAME}/ {
|
||||||
proxy_pass http://plane-minio:9000/uploads/;
|
proxy_pass http://plane-minio:9000/uploads/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,2 @@
|
|||||||
# Google Client ID for Google OAuth
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID=""
|
|
||||||
# Flag to toggle OAuth
|
# Flag to toggle OAuth
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
NEXT_PUBLIC_ENABLE_OAUTH=0
|
@ -33,7 +33,7 @@ export const SignInView = observer(() => {
|
|||||||
const onSignInSuccess = (response: any) => {
|
const onSignInSuccess = (response: any) => {
|
||||||
const isOnboarded = response?.user?.onboarding_step?.profile_complete || false;
|
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);
|
userStore.setCurrentUser(response?.user);
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export const SignInView = observer(() => {
|
|||||||
router.push(`/onboarding?next_path=${nextPath}`);
|
router.push(`/onboarding?next_path=${nextPath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push((nextPath ?? "/").toString());
|
router.push((nextPath ?? "/login").toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
const handleGoogleSignIn = async ({ clientId, credential }: any) => {
|
||||||
|
@ -1 +1 @@
|
|||||||
export * from "./home";
|
export * from "./login";
|
||||||
|
@ -4,7 +4,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
|||||||
// components
|
// components
|
||||||
import { SignInView, UserLoggedIn } from "components/accounts";
|
import { SignInView, UserLoggedIn } from "components/accounts";
|
||||||
|
|
||||||
export const HomeView = observer(() => {
|
export const LoginView = observer(() => {
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
|
|
||||||
if (!userStore.currentUser) return <SignInView />;
|
if (!userStore.currentUser) return <SignInView />;
|
@ -1,8 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
// components
|
|
||||||
import { HomeView } from "components/views";
|
|
||||||
|
|
||||||
const HomePage = () => <HomeView />;
|
|
||||||
|
|
||||||
export default HomePage;
|
|
8
space/pages/login/index.tsx
Normal file
8
space/pages/login/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { LoginView } from "components/views";
|
||||||
|
|
||||||
|
const LoginPage = () => <LoginView />;
|
||||||
|
|
||||||
|
export default LoginPage;
|
@ -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
|
# Enable/Disable OAUTH - default 0 for selfhosted instance
|
||||||
NEXT_PUBLIC_ENABLE_OAUTH=0
|
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
|
# 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";
|
import { generateBarColor } from "helpers/analytics.helper";
|
||||||
// types
|
// types
|
||||||
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
import { IAnalyticsParams, IAnalyticsResponse } from "types";
|
||||||
// constants
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse;
|
analytics: IAnalyticsResponse;
|
||||||
|
@ -15,17 +15,19 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
<div className="divide-y divide-custom-border-200">
|
<div className="divide-y divide-custom-border-200">
|
||||||
<div>
|
<div>
|
||||||
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
<h6 className="px-3 text-base font-medium">Pending issues</h6>
|
||||||
{defaultAnalytics.pending_issue_user.length > 0 ? (
|
{defaultAnalytics.pending_issue_user && defaultAnalytics.pending_issue_user.length > 0 ? (
|
||||||
<BarGraph
|
<BarGraph
|
||||||
data={defaultAnalytics.pending_issue_user}
|
data={defaultAnalytics.pending_issue_user}
|
||||||
indexBy="assignees__display_name"
|
indexBy="assignees__id"
|
||||||
keys={["count"]}
|
keys={["count"]}
|
||||||
height="250px"
|
height="250px"
|
||||||
colors={() => `#f97316`}
|
colors={() => `#f97316`}
|
||||||
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) => d.count)}
|
customYAxisTickValues={defaultAnalytics.pending_issue_user.map((d) =>
|
||||||
|
d.count > 0 ? d.count : 50
|
||||||
|
)}
|
||||||
tooltip={(datum) => {
|
tooltip={(datum) => {
|
||||||
const assignee = defaultAnalytics.pending_issue_user.find(
|
const assignee = defaultAnalytics.pending_issue_user.find(
|
||||||
(a) => a.assignees__display_name === `${datum.indexValue}`
|
(a) => a.assignees__id === `${datum.indexValue}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -39,10 +41,9 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
}}
|
}}
|
||||||
axisBottom={{
|
axisBottom={{
|
||||||
renderTick: (datum) => {
|
renderTick: (datum) => {
|
||||||
const avatar =
|
const assignee = defaultAnalytics.pending_issue_user[datum.tickIndex] ?? "";
|
||||||
defaultAnalytics.pending_issue_user[datum.tickIndex]?.assignees__avatar ?? "";
|
|
||||||
|
|
||||||
if (avatar && avatar !== "")
|
if (assignee && assignee?.assignees__avatar && assignee?.assignees__avatar !== "")
|
||||||
return (
|
return (
|
||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
<image
|
<image
|
||||||
@ -50,7 +51,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
y={10}
|
y={10}
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
xlinkHref={avatar}
|
xlinkHref={assignee?.assignees__avatar}
|
||||||
style={{ clipPath: "circle(50%)" }}
|
style={{ clipPath: "circle(50%)" }}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
@ -60,7 +61,7 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
|||||||
<g transform={`translate(${datum.x},${datum.y})`}>
|
<g transform={`translate(${datum.x},${datum.y})`}>
|
||||||
<circle cy={18} r={8} fill="#374151" />
|
<circle cy={18} r={8} fill="#374151" />
|
||||||
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
<text x={0} y={21} textAnchor="middle" fontSize={9} fill="#ffffff">
|
||||||
{datum.value ? `${datum.value}`.toUpperCase()[0] : "?"}
|
{datum.value ? `${assignee.assignees__display_name}`.toUpperCase()[0] : "?"}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,41 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
// hook
|
||||||
|
import useEstimateOption from "hooks/use-estimate-option";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
// icons
|
// icons
|
||||||
import { Icon, Tooltip } from "components/ui";
|
import { Icon, Tooltip } from "components/ui";
|
||||||
import { CopyPlus } from "lucide-react";
|
import {
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
TagIcon,
|
||||||
import { BlockedIcon, BlockerIcon, RelatedIcon } from "components/icons";
|
CopyPlus,
|
||||||
|
Calendar,
|
||||||
|
Link2Icon,
|
||||||
|
RocketIcon,
|
||||||
|
Users2Icon,
|
||||||
|
ArchiveIcon,
|
||||||
|
PaperclipIcon,
|
||||||
|
ContrastIcon,
|
||||||
|
TriangleIcon,
|
||||||
|
LayoutGridIcon,
|
||||||
|
SignalMediumIcon,
|
||||||
|
MessageSquareIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
BlockedIcon,
|
||||||
|
BlockerIcon,
|
||||||
|
RelatedIcon,
|
||||||
|
StackedLayersHorizontalIcon,
|
||||||
|
} from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
import { capitalizeFirstLetter } from "helpers/string.helper";
|
import { capitalizeFirstLetter } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { IIssueActivity } from "types";
|
import { IIssueActivity } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -30,7 +56,7 @@ const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
{activity.issue_detail
|
{activity.issue_detail
|
||||||
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
|
||||||
: "Issue"}
|
: "Issue"}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@ -52,6 +78,38 @@ 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 EstimatePoint = ({ point }: { point: string }) => {
|
||||||
|
const { estimateValue, isEstimateActive } = useEstimateOption(Number(point));
|
||||||
|
const currentPoint = Number(point) + 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="font-medium text-custom-text-100">
|
||||||
|
{isEstimateActive
|
||||||
|
? estimateValue
|
||||||
|
: `${currentPoint} ${currentPoint > 1 ? "points" : "point"}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
message: (
|
message: (
|
||||||
@ -91,14 +149,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="group" className="!text-2xl" aria-hidden="true" />,
|
icon: <Users2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
archived_at: {
|
archived_at: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.new_value === "restore") return "restored the issue.";
|
if (activity.new_value === "restore") return "restored the issue.";
|
||||||
else return "archived the issue.";
|
else return "archived the issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="archive" className="!text-2xl" aria-hidden="true" />,
|
icon: <ArchiveIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
attachment: {
|
attachment: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -113,7 +171,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
attachment
|
attachment
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -137,7 +195,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="attach_file" className="!text-2xl" aria-hidden="true" />,
|
icon: <PaperclipIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
blocking: {
|
blocking: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
@ -228,7 +286,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -243,7 +301,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -258,12 +316,12 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.old_value}
|
{activity.old_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="contrast" className="!text-2xl" aria-hidden="true" />,
|
icon: <ContrastIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -278,7 +336,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
estimate_point: {
|
estimate_point: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -298,8 +356,7 @@ const activityDetails: {
|
|||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the estimate point to{" "}
|
set the estimate point to <EstimatePoint point={activity.new_value} />
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
@ -310,14 +367,14 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="change_history" className="!text-2xl" aria-hidden="true" />,
|
icon: <TriangleIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
issue: {
|
issue: {
|
||||||
message: (activity) => {
|
message: (activity) => {
|
||||||
if (activity.verb === "created") return "created the issue.";
|
if (activity.verb === "created") return "created the issue.";
|
||||||
else return "deleted an issue.";
|
else return "deleted an issue.";
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="stack" className="!text-2xl" aria-hidden="true" />,
|
icon: <StackedLayersHorizontalIcon width={12} height={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -325,14 +382,8 @@ const activityDetails: {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added a new label{" "}
|
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="inline-flex items-center gap-2 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||||
<span
|
<LabelPill labelId={activity.new_identifier ?? ""} />
|
||||||
className="h-1.5 w-1.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#000000",
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -348,13 +399,7 @@ const activityDetails: {
|
|||||||
<>
|
<>
|
||||||
removed the label{" "}
|
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="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
|
||||||
<span
|
<LabelPill labelId={activity.old_identifier ?? ""} />
|
||||||
className="h-1.5 w-1.5 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#000000",
|
|
||||||
}}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||||
</span>
|
</span>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
@ -366,7 +411,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="sell" className="!text-2xl" aria-hidden="true" />,
|
icon: <TagIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -381,7 +426,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -403,7 +448,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -425,7 +470,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
link
|
link
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
{showIssue && (
|
{showIssue && (
|
||||||
<>
|
<>
|
||||||
@ -437,7 +482,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="link" className="!text-2xl" aria-hidden="true" />,
|
icon: <Link2Icon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: (activity, showIssue, workspaceSlug) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
@ -452,7 +497,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -467,7 +512,7 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.new_value}
|
{activity.new_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -482,12 +527,12 @@ const activityDetails: {
|
|||||||
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
>
|
>
|
||||||
{activity.old_value}
|
{activity.old_value}
|
||||||
<Icon iconName="launch" className="!text-xs" />
|
<RocketIcon size={12} color="#6b7280" />
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="dataset" className="!text-2xl" aria-hidden="true" />,
|
icon: <Icon iconName="dataset" className="!text-xs !text-[#6b7280]" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -502,7 +547,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="chat" className="!text-2xl" aria-hidden="true" />,
|
icon: <MessageSquareIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
parent: {
|
parent: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -535,7 +580,13 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="supervised_user_circle" className="!text-2xl" aria-hidden="true" />,
|
icon: (
|
||||||
|
<Icon
|
||||||
|
iconName="supervised_user_circle"
|
||||||
|
className="!text-xs !text-[#6b7280]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
priority: {
|
priority: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -553,7 +604,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Icon iconName="signal_cellular_alt" className="!text-2xl" aria-hidden="true" />,
|
icon: <SignalMediumIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
start_date: {
|
start_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -587,7 +638,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
message: (activity, showIssue) => (
|
message: (activity, showIssue) => (
|
||||||
@ -603,7 +654,7 @@ const activityDetails: {
|
|||||||
.
|
.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
icon: <Squares2X2Icon className="h-6 w-6 text-custom-sidebar-200" aria-hidden="true" />,
|
icon: <LayoutGridIcon size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
message: (activity, showIssue) => {
|
message: (activity, showIssue) => {
|
||||||
@ -637,7 +688,7 @@ const activityDetails: {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: <Icon iconName="calendar_today" className="!text-2xl" aria-hidden="true" />,
|
icon: <Calendar size={12} color="#6b7280" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +167,9 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setFilters({
|
setFilters({
|
||||||
assignees: filters.assignees?.filter((p: any) => p !== memberId),
|
subscriber: filters.subscriber?.filter(
|
||||||
|
(p: any) => p !== memberId
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./date-filter-modal";
|
export * from "./date-filter-modal";
|
||||||
export * from "./date-filter-select";
|
export * from "./date-filter-select";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
|
export * from "./workspace-filters-list";
|
||||||
export * from "./issues-view-filter";
|
export * from "./issues-view-filter";
|
||||||
|
@ -67,7 +67,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
const isDraftIssues = router.pathname.includes("draft-issues");
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displayFilters,
|
displayFilters,
|
||||||
@ -93,7 +93,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
key={option.type}
|
key={option.type}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<span className="capitalize">{replaceUnderscoreIfSnakeCase(option.type)} View</span>
|
<span className="capitalize">
|
||||||
|
{replaceUnderscoreIfSnakeCase(option.type)} Layout
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
position="bottom"
|
position="bottom"
|
||||||
>
|
>
|
||||||
@ -228,6 +230,9 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
if (option.key === "project") return null;
|
if (option.key === "project") return null;
|
||||||
|
|
||||||
|
if (isDraftIssues && option.key === "state_detail.group")
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
@ -272,33 +277,35 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between">
|
{!isArchivedIssues && (
|
||||||
<h4 className="text-custom-text-200">Issue type</h4>
|
<div className="flex items-center justify-between">
|
||||||
<div className="w-28">
|
<h4 className="text-custom-text-200">Issue type</h4>
|
||||||
<CustomMenu
|
<div className="w-28">
|
||||||
label={
|
<CustomMenu
|
||||||
FILTER_ISSUE_OPTIONS.find(
|
label={
|
||||||
(option) => option.key === displayFilters.type
|
FILTER_ISSUE_OPTIONS.find(
|
||||||
)?.name ?? "Select"
|
(option) => option.key === displayFilters.type
|
||||||
}
|
)?.name ?? "Select"
|
||||||
className="!w-full"
|
}
|
||||||
buttonClassName="w-full"
|
className="!w-full"
|
||||||
>
|
buttonClassName="w-full"
|
||||||
{FILTER_ISSUE_OPTIONS.map((option) => (
|
>
|
||||||
<CustomMenu.MenuItem
|
{FILTER_ISSUE_OPTIONS.map((option) => (
|
||||||
key={option.key}
|
<CustomMenu.MenuItem
|
||||||
onClick={() =>
|
key={option.key}
|
||||||
setDisplayFilters({
|
onClick={() =>
|
||||||
type: option.key,
|
setDisplayFilters({
|
||||||
})
|
type: option.key,
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
{option.name}
|
>
|
||||||
</CustomMenu.MenuItem>
|
{option.name}
|
||||||
))}
|
</CustomMenu.MenuItem>
|
||||||
</CustomMenu>
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{displayFilters.layout !== "calendar" &&
|
{displayFilters.layout !== "calendar" &&
|
||||||
displayFilters.layout !== "spreadsheet" && (
|
displayFilters.layout !== "spreadsheet" && (
|
||||||
@ -318,7 +325,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
displayFilters.layout !== "spreadsheet" &&
|
displayFilters.layout !== "spreadsheet" &&
|
||||||
displayFilters.layout !== "gantt_chart" && (
|
displayFilters.layout !== "gantt_chart" && (
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<div className="w-28">
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
value={displayFilters.show_empty_groups ?? true}
|
value={displayFilters.show_empty_groups ?? true}
|
||||||
|
364
web/components/core/filters/workspace-filters-list.tsx
Normal file
364
web/components/core/filters/workspace-filters-list.tsx
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// icons
|
||||||
|
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { PriorityIcon, StateGroupIcon } from "components/icons";
|
||||||
|
// ui
|
||||||
|
import { Avatar } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
|
// helpers
|
||||||
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
// types
|
||||||
|
import {
|
||||||
|
IIssueLabels,
|
||||||
|
IProject,
|
||||||
|
IUserLite,
|
||||||
|
IWorkspaceIssueFilterOptions,
|
||||||
|
TStateGroups,
|
||||||
|
} from "types";
|
||||||
|
// constants
|
||||||
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
filters: Partial<IWorkspaceIssueFilterOptions>;
|
||||||
|
setFilters: (updatedFilter: Partial<IWorkspaceIssueFilterOptions>) => void;
|
||||||
|
clearAllFilters: (...args: any) => void;
|
||||||
|
labels: IIssueLabels[] | undefined;
|
||||||
|
members: IUserLite[] | undefined;
|
||||||
|
stateGroup: string[] | undefined;
|
||||||
|
project?: IProject[] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceFiltersList: React.FC<Props> = ({
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
clearAllFilters,
|
||||||
|
labels,
|
||||||
|
members,
|
||||||
|
stateGroup,
|
||||||
|
project,
|
||||||
|
}) => {
|
||||||
|
if (!filters) return <></>;
|
||||||
|
|
||||||
|
const nullFilters = Object.keys(filters).filter(
|
||||||
|
(key) => filters[key as keyof IWorkspaceIssueFilterOptions] === null
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-wrap items-center gap-2 text-xs">
|
||||||
|
{Object.keys(filters).map((filterKey) => {
|
||||||
|
const key = filterKey as keyof typeof filters;
|
||||||
|
|
||||||
|
if (filters[key] === null || (filters[key]?.length ?? 0) <= 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-x-2 rounded-full border border-custom-border-200 bg-custom-background-80 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="capitalize text-custom-text-200">
|
||||||
|
{key === "target_date" ? "Due Date" : replaceUnderscoreIfSnakeCase(key)}:
|
||||||
|
</span>
|
||||||
|
{filters[key] === null || (filters[key]?.length ?? 0) <= 0 ? (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 font-medium">None</span>
|
||||||
|
) : Array.isArray(filters[key]) ? (
|
||||||
|
<div className="space-x-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{key === "state_group"
|
||||||
|
? filters.state_group?.map((stateGroup) => {
|
||||||
|
const group = stateGroup as TStateGroups;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
key={group}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize"
|
||||||
|
style={{
|
||||||
|
color: STATE_GROUP_COLORS[group],
|
||||||
|
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<StateGroupIcon stateGroup={group} color={undefined} />
|
||||||
|
</span>
|
||||||
|
<span>{group}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
state_group: filters.state_group?.filter((g) => g !== group),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "priority"
|
||||||
|
? filters.priority?.map((priority: any) => (
|
||||||
|
<p
|
||||||
|
key={priority}
|
||||||
|
className={`inline-flex items-center gap-x-1 rounded-full px-2 py-0.5 capitalize ${
|
||||||
|
priority === "urgent"
|
||||||
|
? "bg-red-500/20 text-red-500"
|
||||||
|
: priority === "high"
|
||||||
|
? "bg-orange-500/20 text-orange-500"
|
||||||
|
: priority === "medium"
|
||||||
|
? "bg-yellow-500/20 text-yellow-500"
|
||||||
|
: priority === "low"
|
||||||
|
? "bg-green-500/20 text-green-500"
|
||||||
|
: "bg-custom-background-90 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<PriorityIcon priority={priority} />
|
||||||
|
</span>
|
||||||
|
<span>{priority === "null" ? "None" : priority}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
priority: filters.priority?.filter((p: any) => p !== priority),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
: 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
|
||||||
|
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 === "created_by"
|
||||||
|
? filters.created_by?.map((memberId: string) => {
|
||||||
|
const member = members?.find((m) => m.id === memberId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${memberId}-${key}`}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full bg-custom-background-90 px-1 capitalize"
|
||||||
|
>
|
||||||
|
<Avatar user={member} />
|
||||||
|
<span>{member?.display_name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
created_by: filters.created_by?.filter(
|
||||||
|
(p: any) => p !== memberId
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "labels"
|
||||||
|
? filters.labels?.map((labelId: string) => {
|
||||||
|
const label = labels?.find((l) => l.id === labelId);
|
||||||
|
|
||||||
|
if (!label) return null;
|
||||||
|
const color = label.color !== "" ? label.color : "#0f172a";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
backgroundColor: `${color}20`, // add 20% opacity
|
||||||
|
}}
|
||||||
|
key={labelId}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-1.5 w-1.5 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>{label.name}</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
labels: filters.labels?.filter((l: any) => l !== labelId),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon
|
||||||
|
className="h-3 w-3"
|
||||||
|
style={{
|
||||||
|
color: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "start_date"
|
||||||
|
? filters.start_date?.map((date: string) => {
|
||||||
|
if (filters.start_date && filters.start_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
start_date: filters.start_date?.filter((d: any) => d !== date),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "target_date"
|
||||||
|
? filters.target_date?.map((date: string) => {
|
||||||
|
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||||
|
|
||||||
|
const splitDate = date.split(";");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className="inline-flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-100 px-1 py-0.5"
|
||||||
|
>
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full" />
|
||||||
|
<span className="capitalize">
|
||||||
|
{splitDate[1]} {renderShortDateWithYearFormat(splitDate[0])}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
target_date: filters.target_date?.filter((d: any) => d !== date),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: key === "project"
|
||||||
|
? filters.project?.map((projectId) => {
|
||||||
|
const currentProject = project?.find((p) => p.id === 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"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-x-1 capitalize">
|
||||||
|
{filters[key as keyof typeof filters]}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({
|
||||||
|
[key]: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{Object.keys(filters).length > 0 && nullFilters.length !== Object.keys(filters).length && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="flex items-center gap-x-1 rounded-full border border-custom-border-200 bg-custom-background-80 px-3 py-1.5 text-xs"
|
||||||
|
>
|
||||||
|
<span>Clear all filters</span>
|
||||||
|
<XMarkIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -12,6 +12,7 @@ import stateService from "services/state.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
AllLists,
|
AllLists,
|
||||||
@ -86,6 +87,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
|
|
||||||
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
const { groupedIssues, isEmpty, displayFilters } = viewProps;
|
||||||
|
|
||||||
|
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const { data: stateGroups } = useSWR(
|
||||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||||
workspaceSlug
|
workspaceSlug
|
||||||
@ -174,6 +177,8 @@ export const AllViews: React.FC<Props> = ({
|
|||||||
) : displayFilters?.layout === "spreadsheet" ? (
|
) : displayFilters?.layout === "spreadsheet" ? (
|
||||||
<SpreadsheetView
|
<SpreadsheetView
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
spreadsheetIssues={spreadsheetIssues}
|
||||||
|
mutateIssues={mutateIssues}
|
||||||
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
user={user}
|
||||||
|
@ -76,7 +76,7 @@ export const AllBoards: React.FC<Props> = ({
|
|||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
{groupedIssues ? (
|
{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) => {
|
{Object.keys(groupedIssues).map((singleGroup, index) => {
|
||||||
const currentState =
|
const currentState =
|
||||||
displayFilters?.group_by === "state"
|
displayFilters?.group_by === "state"
|
||||||
|
@ -20,7 +20,7 @@ import { renderEmoji } from "helpers/emoji.helper";
|
|||||||
// types
|
// types
|
||||||
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
|
||||||
// fetch-keys
|
// 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
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -59,6 +59,15 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
: null
|
: 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(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
projectId &&
|
projectId &&
|
||||||
@ -82,7 +91,10 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title =
|
||||||
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
@ -137,7 +149,9 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
className="h-3.5 w-3.5 flex-shrink-0 rounded-full"
|
||||||
|
@ -2,3 +2,4 @@ export * from "./all-boards";
|
|||||||
export * from "./board-header";
|
export * from "./board-header";
|
||||||
export * from "./single-board";
|
export * from "./single-board";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// components
|
||||||
|
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>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
|
||||||
|
{projectDetails?.identifier ?? "..."}
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex flex-col border-[0.5px] border-custom-border-100 justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow-custom-shadow-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
{props.isOpen && (
|
||||||
|
<p className="text-xs ml-3 italic text-custom-text-200">
|
||||||
|
Press {"'"}Enter{"'"} to add another issue
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
|||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -34,31 +34,39 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = (props) => {
|
||||||
addIssueToGroup,
|
const {
|
||||||
currentState,
|
addIssueToGroup,
|
||||||
groupTitle,
|
currentState,
|
||||||
disableUserActions,
|
groupTitle,
|
||||||
disableAddIssueOption = false,
|
disableUserActions,
|
||||||
dragDisabled,
|
disableAddIssueOption = false,
|
||||||
handleIssueAction,
|
dragDisabled,
|
||||||
handleDraftIssueAction,
|
handleIssueAction,
|
||||||
handleTrashBox,
|
handleDraftIssueAction,
|
||||||
openIssuesListModal,
|
handleTrashBox,
|
||||||
handleMyIssueOpen,
|
openIssuesListModal,
|
||||||
removeIssue,
|
handleMyIssueOpen,
|
||||||
user,
|
removeIssue,
|
||||||
userAuth,
|
user,
|
||||||
viewProps,
|
userAuth,
|
||||||
}) => {
|
viewProps,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { cycleId, moduleId } = router.query;
|
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";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
// Check if it has at least 4 tickets since it is enough to accommodate the Calendar height
|
||||||
@ -67,6 +75,27 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateClick = () => {
|
||||||
|
setIsInlineCreateIssueFormOpen(true);
|
||||||
|
scrollToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
@ -115,6 +144,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
id={`board-list-${groupTitle}`}
|
||||||
className={`pt-3 ${
|
className={`pt-3 ${
|
||||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
} `}
|
} `}
|
||||||
@ -134,6 +164,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
type={type}
|
type={type}
|
||||||
index={index}
|
index={index}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
makeIssueCopy={() => handleIssueAction(issue, "copy")}
|
||||||
@ -169,6 +200,20 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<>{provided.placeholder}</>
|
<>{provided.placeholder}</>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<BoardInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
onSuccess={() => scrollToBottom()}
|
||||||
|
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>
|
</div>
|
||||||
{displayFilters?.group_by !== "created_by" && (
|
{displayFilters?.group_by !== "created_by" && (
|
||||||
<div>
|
<div>
|
||||||
@ -177,7 +222,11 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
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" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -197,7 +246,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
|
@ -56,6 +56,7 @@ type Props = {
|
|||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
snapshot: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
groupTitle?: string;
|
groupTitle?: string;
|
||||||
index: number;
|
index: number;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
@ -77,6 +78,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
index,
|
index,
|
||||||
editIssue,
|
editIssue,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
@ -104,7 +106,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
const { displayFilters, properties, mutateIssues } = viewProps;
|
const { displayFilters, properties, mutateIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const isDraftIssue = router.pathname.includes("draft-issues");
|
const isDraftIssue = router.pathname.includes("draft-issues");
|
||||||
|
|
||||||
@ -452,6 +454,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
<StateSelect
|
<StateSelect
|
||||||
value={issue.state_detail}
|
value={issue.state_detail}
|
||||||
onChange={handleStateChange}
|
onChange={handleStateChange}
|
||||||
|
projectId={projectId}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
/>
|
/>
|
||||||
@ -479,6 +482,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
{properties.labels && issue.labels.length > 0 && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<LabelSelect
|
<LabelSelect
|
||||||
value={issue.labels}
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleLabelChange}
|
onChange={handleLabelChange}
|
||||||
labelsDetails={issue.label_details}
|
labelsDetails={issue.label_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
@ -489,6 +493,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
|||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<MembersSelect
|
<MembersSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleAssigneeChange}
|
onChange={handleAssigneeChange}
|
||||||
membersDetails={issue.assignee_details}
|
membersDetails={issue.assignee_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
|
@ -5,25 +5,12 @@ import { Popover, Transition } from "@headlessui/react";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, ToggleSwitch } from "components/ui";
|
import { CustomMenu, ToggleSwitch } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
// helpers
|
// helpers
|
||||||
import {
|
import {
|
||||||
addMonths,
|
|
||||||
addSevenDaysToDate,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
getCurrentWeekEndDate,
|
|
||||||
getCurrentWeekStartDate,
|
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isSameYear,
|
isSameYear,
|
||||||
lastDayOfWeek,
|
|
||||||
startOfWeek,
|
|
||||||
subtract7DaysToDate,
|
|
||||||
subtractMonths,
|
|
||||||
updateDateWithMonth,
|
updateDateWithMonth,
|
||||||
updateDateWithYear,
|
updateDateWithYear,
|
||||||
} from "helpers/calendar.helper";
|
} from "helpers/calendar.helper";
|
||||||
@ -31,190 +18,136 @@ import {
|
|||||||
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
|
import { MONTHS_LIST, YEARS_LIST } from "constants/calendar";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isMonthlyView: boolean;
|
|
||||||
setIsMonthlyView: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
currentDate: Date;
|
currentDate: Date;
|
||||||
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
setCurrentDate: React.Dispatch<React.SetStateAction<Date>>;
|
||||||
showWeekEnds: boolean;
|
showWeekEnds: boolean;
|
||||||
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowWeekEnds: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
changeDateRange: (startDate: Date, endDate: Date) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CalendarHeader: React.FC<Props> = ({
|
export const CalendarHeader: React.FC<Props> = ({
|
||||||
setIsMonthlyView,
|
|
||||||
isMonthlyView,
|
|
||||||
currentDate,
|
currentDate,
|
||||||
setCurrentDate,
|
setCurrentDate,
|
||||||
showWeekEnds,
|
showWeekEnds,
|
||||||
setShowWeekEnds,
|
setShowWeekEnds,
|
||||||
changeDateRange,
|
}) => (
|
||||||
}) => {
|
<div className="mb-4 flex items-center justify-between">
|
||||||
const updateDate = (date: Date) => {
|
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
||||||
setCurrentDate(date);
|
<Popover className="flex h-full items-center justify-start rounded-lg">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Popover.Button>
|
||||||
|
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
||||||
|
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
||||||
|
<span>{formatDate(currentDate, "yyyy")}</span>
|
||||||
|
</div>
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
changeDateRange(startOfWeek(date), lastDayOfWeek(date));
|
<Transition
|
||||||
};
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
return (
|
enterFrom="opacity-0 translate-y-1"
|
||||||
<div className="mb-4 flex items-center justify-between">
|
enterTo="opacity-100 translate-y-0"
|
||||||
<div className="relative flex h-full w-full items-center justify-start gap-2 text-sm ">
|
leave="transition ease-in duration-150"
|
||||||
<Popover className="flex h-full items-center justify-start rounded-lg">
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
{({ open }) => (
|
leaveTo="opacity-0 translate-y-1"
|
||||||
<>
|
>
|
||||||
<Popover.Button>
|
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
||||||
<div className="flex items-center justify-center gap-2 text-2xl font-semibold text-custom-text-100">
|
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
||||||
<span>{formatDate(currentDate, "Month")}</span>{" "}
|
{YEARS_LIST.map((year) => (
|
||||||
<span>{formatDate(currentDate, "yyyy")}</span>
|
<button
|
||||||
|
onClick={() => setCurrentDate(updateDateWithYear(year.label, currentDate))}
|
||||||
|
className={` ${
|
||||||
|
isSameYear(year.value, currentDate)
|
||||||
|
? "text-sm font-medium text-custom-text-100"
|
||||||
|
: "text-xs text-custom-text-200 "
|
||||||
|
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
||||||
|
>
|
||||||
|
{year.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Popover.Button>
|
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
||||||
|
{MONTHS_LIST.map((month) => (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentDate(updateDateWithMonth(`${month.value}`, currentDate))
|
||||||
|
}
|
||||||
|
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
||||||
|
isSameMonth(`${month.value}`, currentDate)
|
||||||
|
? "font-medium text-custom-text-100"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
|
||||||
<Transition
|
<div className="flex items-center gap-2">
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute top-10 left-0 z-20 flex w-full max-w-xs transform flex-col overflow-hidden rounded-[10px] bg-custom-background-80 shadow-lg">
|
|
||||||
<div className="flex items-center justify-center gap-5 px-2 py-2 text-sm">
|
|
||||||
{YEARS_LIST.map((year) => (
|
|
||||||
<button
|
|
||||||
onClick={() => updateDate(updateDateWithYear(year.label, currentDate))}
|
|
||||||
className={` ${
|
|
||||||
isSameYear(year.value, currentDate)
|
|
||||||
? "text-sm font-medium text-custom-text-100"
|
|
||||||
: "text-xs text-custom-text-200 "
|
|
||||||
} hover:text-sm hover:font-medium hover:text-custom-text-100`}
|
|
||||||
>
|
|
||||||
{year.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-4 border-t border-custom-border-200 px-2">
|
|
||||||
{MONTHS_LIST.map((month) => (
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
updateDate(updateDateWithMonth(`${month.value}`, currentDate))
|
|
||||||
}
|
|
||||||
className={`px-2 py-2 text-xs text-custom-text-200 hover:font-medium hover:text-custom-text-100 ${
|
|
||||||
isSameMonth(`${month.value}`, currentDate)
|
|
||||||
? "font-medium text-custom-text-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{month.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
if (isMonthlyView) {
|
|
||||||
updateDate(subtractMonths(currentDate, 1));
|
|
||||||
} else {
|
|
||||||
setCurrentDate(subtract7DaysToDate(currentDate));
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(subtract7DaysToDate(currentDate)),
|
|
||||||
getCurrentWeekEndDate(subtract7DaysToDate(currentDate))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
if (isMonthlyView) {
|
|
||||||
updateDate(addMonths(currentDate, 1));
|
|
||||||
} else {
|
|
||||||
setCurrentDate(addSevenDaysToDate(currentDate));
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(addSevenDaysToDate(currentDate)),
|
|
||||||
getCurrentWeekEndDate(addSevenDaysToDate(currentDate))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full items-center justify-end gap-2">
|
|
||||||
<button
|
<button
|
||||||
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isMonthlyView) {
|
const previousMonthYear =
|
||||||
updateDate(new Date());
|
currentDate.getMonth() === 0
|
||||||
} else {
|
? currentDate.getFullYear() - 1
|
||||||
setCurrentDate(new Date());
|
: currentDate.getFullYear();
|
||||||
changeDateRange(
|
const previousMonthMonth =
|
||||||
getCurrentWeekStartDate(new Date()),
|
currentDate.getMonth() === 0 ? 11 : currentDate.getMonth() - 1;
|
||||||
getCurrentWeekEndDate(new Date())
|
|
||||||
);
|
const previousMonthFirstDate = new Date(previousMonthYear, previousMonthMonth, 1);
|
||||||
}
|
|
||||||
|
setCurrentDate(previousMonthFirstDate);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Today
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
const nextMonthYear =
|
||||||
|
currentDate.getMonth() === 11
|
||||||
|
? currentDate.getFullYear() + 1
|
||||||
|
: currentDate.getFullYear();
|
||||||
|
const nextMonthMonth = (currentDate.getMonth() + 1) % 12;
|
||||||
|
|
||||||
<CustomMenu
|
const nextMonthFirstDate = new Date(nextMonthYear, nextMonthMonth, 1);
|
||||||
customButton={
|
|
||||||
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none ">
|
setCurrentDate(nextMonthFirstDate);
|
||||||
{isMonthlyView ? "Monthly" : "Weekly"}
|
}}
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
onClick={() => {
|
</button>
|
||||||
setIsMonthlyView(true);
|
|
||||||
changeDateRange(startOfWeek(currentDate), lastDayOfWeek(currentDate));
|
|
||||||
}}
|
|
||||||
className="w-52 text-sm text-custom-text-200"
|
|
||||||
>
|
|
||||||
<div className="flex w-full max-w-[260px] items-center justify-between gap-2">
|
|
||||||
<span className="flex items-center gap-2">Monthly View</span>
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-100" : "opacity-0"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setIsMonthlyView(false);
|
|
||||||
changeDateRange(
|
|
||||||
getCurrentWeekStartDate(currentDate),
|
|
||||||
getCurrentWeekEndDate(currentDate)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
className="w-52 text-sm text-custom-text-200"
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center justify-between gap-2">
|
|
||||||
<span className="flex items-center gap-2">Weekly View</span>
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-4 w-4 flex-shrink-0 ${isMonthlyView ? "opacity-0" : "opacity-100"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<div className="mt-1 flex w-52 items-center justify-between border-t border-custom-border-200 py-2 px-1 text-sm text-custom-text-200">
|
|
||||||
<h4>Show weekends</h4>
|
|
||||||
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
|
||||||
</div>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
};
|
<div className="flex w-full items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none"
|
||||||
|
onClick={() => setCurrentDate(new Date())}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<CustomMenu
|
||||||
|
customButton={
|
||||||
|
<div className="group flex cursor-pointer items-center gap-2 rounded-md border border-custom-border-200 px-3 py-1 text-sm hover:bg-custom-background-80 hover:text-custom-text-100 focus:outline-none">
|
||||||
|
Options
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex w-52 items-center justify-between px-1 text-sm text-custom-text-200">
|
||||||
|
<h4>Show weekends</h4>
|
||||||
|
<ToggleSwitch value={showWeekEnds} onChange={() => setShowWeekEnds(!showWeekEnds)} />
|
||||||
|
</div>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default CalendarHeader;
|
export default CalendarHeader;
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
|
||||||
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, DropResult } from "react-beautiful-dnd";
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
@ -50,31 +46,27 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
const [showWeekEnds, setShowWeekEnds] = useState(false);
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
|
||||||
const [isMonthlyView, setIsMonthlyView] = useState(true);
|
const { calendarIssues, mutateIssues, params, activeMonthDate, setActiveMonthDate } =
|
||||||
|
useCalendarIssuesView();
|
||||||
|
|
||||||
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
|
const [calendarDates, setCalendarDates] = useState<ICalendarRange>({
|
||||||
startDate: startOfWeek(currentDate),
|
startDate: startOfWeek(activeMonthDate),
|
||||||
endDate: lastDayOfWeek(currentDate),
|
endDate: lastDayOfWeek(activeMonthDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { calendarIssues, mutateIssues, params, displayFilters, setDisplayFilters } =
|
const currentViewDays = showWeekEnds
|
||||||
useCalendarIssuesView();
|
? eachDayOfInterval({
|
||||||
|
start: calendarDates.startDate,
|
||||||
const totalDate = eachDayOfInterval({
|
end: calendarDates.endDate,
|
||||||
start: calendarDates.startDate,
|
})
|
||||||
end: calendarDates.endDate,
|
: weekDayInterval({
|
||||||
});
|
start: calendarDates.startDate,
|
||||||
|
end: calendarDates.endDate,
|
||||||
const onlyWeekDays = weekDayInterval({
|
});
|
||||||
start: calendarDates.startDate,
|
|
||||||
end: calendarDates.endDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentViewDays = showWeekEnds ? totalDate : onlyWeekDays;
|
|
||||||
|
|
||||||
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
const currentViewDaysData = currentViewDays.map((date: Date) => {
|
||||||
const filterIssue =
|
const filterIssue =
|
||||||
@ -148,27 +140,12 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
.then(() => mutate(fetchKey));
|
.then(() => mutate(fetchKey));
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeDateRange = (startDate: Date, endDate: Date) => {
|
|
||||||
setCalendarDates({
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
setDisplayFilters({
|
|
||||||
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
|
|
||||||
endDate
|
|
||||||
)};before`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!displayFilters || displayFilters.calendar_date_range === "")
|
setCalendarDates({
|
||||||
setDisplayFilters({
|
startDate: startOfWeek(activeMonthDate),
|
||||||
calendar_date_range: `${renderDateFormat(
|
endDate: lastDayOfWeek(activeMonthDate),
|
||||||
startOfWeek(currentDate)
|
});
|
||||||
)};after,${renderDateFormat(lastDayOfWeek(currentDate))};before`,
|
}, [activeMonthDate]);
|
||||||
});
|
|
||||||
}, [currentDate, displayFilters, setDisplayFilters]);
|
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
@ -183,15 +160,15 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
{calendarIssues ? (
|
{calendarIssues ? (
|
||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<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
|
<CalendarHeader
|
||||||
isMonthlyView={isMonthlyView}
|
|
||||||
setIsMonthlyView={setIsMonthlyView}
|
|
||||||
showWeekEnds={showWeekEnds}
|
showWeekEnds={showWeekEnds}
|
||||||
setShowWeekEnds={setShowWeekEnds}
|
setShowWeekEnds={setShowWeekEnds}
|
||||||
currentDate={currentDate}
|
currentDate={activeMonthDate}
|
||||||
setCurrentDate={setCurrentDate}
|
setCurrentDate={setActiveMonthDate}
|
||||||
changeDateRange={changeDateRange}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -202,30 +179,15 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
{weeks.map((date, index) => (
|
{weeks.map((date, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200 ${
|
className={`flex items-center justify-start gap-2 border-custom-border-200 bg-custom-background-90 p-1.5 text-base font-medium text-custom-text-200`}
|
||||||
!isMonthlyView
|
|
||||||
? showWeekEnds
|
|
||||||
? (index + 1) % 7 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: (index + 1) % 5 === 0
|
|
||||||
? ""
|
|
||||||
: "border-r"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>{formatDate(date, "eee").substring(0, 3)}</span>
|
||||||
{isMonthlyView
|
|
||||||
? formatDate(date, "eee").substring(0, 3)
|
|
||||||
: formatDate(date, "eee")}
|
|
||||||
</span>
|
|
||||||
{!isMonthlyView && <span>{formatDate(date, "d")}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`grid h-full ${isMonthlyView ? "auto-rows-min" : ""} ${
|
className={`grid h-full auto-rows-min ${
|
||||||
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
showWeekEnds ? "grid-cols-7" : "grid-cols-5"
|
||||||
} `}
|
} `}
|
||||||
>
|
>
|
||||||
@ -236,7 +198,6 @@ export const CalendarView: React.FC<Props> = ({
|
|||||||
date={date}
|
date={date}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
addIssueToDate={addIssueToDate}
|
addIssueToDate={addIssueToDate}
|
||||||
isMonthlyView={isMonthlyView}
|
|
||||||
showWeekEnds={showWeekEnds}
|
showWeekEnds={showWeekEnds}
|
||||||
user={user}
|
user={user}
|
||||||
isNotAllowed={isNotAllowed}
|
isNotAllowed={isNotAllowed}
|
||||||
|
@ -2,3 +2,4 @@ export * from "./calendar-header";
|
|||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./single-date";
|
export * from "./single-date";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -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 pr-2 py-2.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 top-10 transition-all z-20 w-full max-w-[calc(100%-1.25rem)] ${
|
||||||
|
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||||
|
} right-2.5`}
|
||||||
|
>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
{...props}
|
||||||
|
className="flex w-full px-1.5 border-[0.5px] border-custom-border-100 rounded z-50 items-center gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm 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";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// component
|
// component
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { SingleCalendarIssue } from "./single-issue";
|
import { SingleCalendarIssue } from "./single-issue";
|
||||||
|
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||||
// icons
|
// icons
|
||||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||||
// helper
|
// helper
|
||||||
@ -20,23 +24,21 @@ type Props = {
|
|||||||
issues: IIssue[];
|
issues: IIssue[];
|
||||||
};
|
};
|
||||||
addIssueToDate: (date: string) => void;
|
addIssueToDate: (date: string) => void;
|
||||||
isMonthlyView: boolean;
|
|
||||||
showWeekEnds: boolean;
|
showWeekEnds: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleCalendarDate: React.FC<Props> = ({
|
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||||
handleIssueAction,
|
const { handleIssueAction, date, index, showWeekEnds, user, isNotAllowed } = props;
|
||||||
date,
|
|
||||||
index,
|
const router = useRouter();
|
||||||
addIssueToDate,
|
const { cycleId, moduleId } = router.query;
|
||||||
isMonthlyView,
|
|
||||||
showWeekEnds,
|
|
||||||
user,
|
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
|
||||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
|
const [formPosition, setFormPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
const totalIssues = date.issues.length;
|
const totalIssues = date.issues.length;
|
||||||
|
|
||||||
@ -48,8 +50,6 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
|
className={`group relative flex min-h-[150px] flex-col gap-1.5 border-t border-custom-border-200 p-2.5 text-left text-sm font-medium hover:bg-custom-background-90 ${
|
||||||
isMonthlyView ? "" : "pt-9"
|
|
||||||
} ${
|
|
||||||
showWeekEnds
|
showWeekEnds
|
||||||
? (index + 1) % 7 === 0
|
? (index + 1) % 7 === 0
|
||||||
? ""
|
? ""
|
||||||
@ -59,48 +59,66 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
: "border-r"
|
: "border-r"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isMonthlyView && <span>{formatDate(new Date(date.date), "d")}</span>}
|
<>
|
||||||
{totalIssues > 0 &&
|
<span>{formatDate(new Date(date.date), "d")}</span>
|
||||||
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
|
{totalIssues > 0 &&
|
||||||
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
date.issues.slice(0, showAllIssues ? totalIssues : 4).map((issue: IIssue, index) => (
|
||||||
{(provided, snapshot) => (
|
<Draggable key={issue.id} draggableId={issue.id} index={index}>
|
||||||
<SingleCalendarIssue
|
{(provided, snapshot) => (
|
||||||
key={index}
|
<SingleCalendarIssue
|
||||||
index={index}
|
key={index}
|
||||||
provided={provided}
|
index={index}
|
||||||
snapshot={snapshot}
|
provided={provided}
|
||||||
issue={issue}
|
snapshot={snapshot}
|
||||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
issue={issue}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
projectId={issue.project_detail.id}
|
||||||
user={user}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
isNotAllowed={isNotAllowed}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
/>
|
user={user}
|
||||||
)}
|
isNotAllowed={isNotAllowed}
|
||||||
</Draggable>
|
/>
|
||||||
))}
|
)}
|
||||||
{totalIssues > 4 && (
|
</Draggable>
|
||||||
<button
|
))}
|
||||||
type="button"
|
|
||||||
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
|
|
||||||
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
|
||||||
>
|
|
||||||
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<CalendarInlineCreateIssueForm
|
||||||
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
|
isOpen={isCreateIssueFormOpen}
|
||||||
>
|
dependencies={[showWeekEnds]}
|
||||||
<button
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
className="flex items-center justify-center gap-1 text-center"
|
prePopulatedData={{
|
||||||
onClick={() => addIssueToDate(date.date)}
|
target_date: date.date,
|
||||||
>
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
Add issue
|
}}
|
||||||
</button>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{provided.placeholder}
|
{totalIssues > 4 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-min whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 px-1.5 py-1 text-xs"
|
||||||
|
onClick={() => setShowAllIssues((prevData) => !prevData)}
|
||||||
|
>
|
||||||
|
{showAllIssues ? "Hide" : totalIssues - 4 + " more"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute top-2 right-2 flex items-center justify-center rounded-md bg-custom-background-80 p-1 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
setIsCreateIssueFormOpen(true);
|
||||||
|
setFormPosition({ x: e.clientX, y: e.clientY });
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center gap-1 text-center"
|
||||||
|
>
|
||||||
|
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
|
Add issue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{provided.placeholder}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
|
@ -41,6 +41,7 @@ type Props = {
|
|||||||
provided: DraggableProvided;
|
provided: DraggableProvided;
|
||||||
snapshot: DraggableStateSnapshot;
|
snapshot: DraggableStateSnapshot;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
@ -52,11 +53,12 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
provided,
|
provided,
|
||||||
snapshot,
|
snapshot,
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
user,
|
user,
|
||||||
isNotAllowed,
|
isNotAllowed,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -310,6 +312,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
{properties.state && (
|
{properties.state && (
|
||||||
<StateSelect
|
<StateSelect
|
||||||
value={issue.state_detail}
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleStateChange}
|
onChange={handleStateChange}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
@ -334,6 +337,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
{properties.labels && issue.labels.length > 0 && (
|
{properties.labels && issue.labels.length > 0 && (
|
||||||
<LabelSelect
|
<LabelSelect
|
||||||
value={issue.labels}
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleLabelChange}
|
onChange={handleLabelChange}
|
||||||
labelsDetails={issue.label_details}
|
labelsDetails={issue.label_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
@ -345,6 +349,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
|||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<MembersSelect
|
<MembersSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleAssigneeChange}
|
onChange={handleAssigneeChange}
|
||||||
membersDetails={issue.assignee_details}
|
membersDetails={issue.assignee_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
|
@ -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 "components/core";
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<>
|
||||||
|
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
|
||||||
|
<h4 className="text-sm 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 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex py-3 px-4 border-[0.5px] border-custom-border-100 mr-2.5 items-center rounded gap-x-2 bg-custom-background-100 shadow-custom-shadow-sm"
|
||||||
|
{...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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -5,3 +5,4 @@ export * from "./list-view";
|
|||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./all-views";
|
export * from "./all-views";
|
||||||
export * from "./issues-view";
|
export * from "./issues-view";
|
||||||
|
export * from "./inline-issue-create-wrapper";
|
||||||
|
273
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
273
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
@ -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) => {
|
||||||
|
await 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -81,7 +81,9 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
const isDraftIssues = router.asPath.includes("draft-issues");
|
|
||||||
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
const isArchivedIssues = router.pathname?.split("/")?.[4] === "archived-issues";
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
|
|
||||||
@ -624,6 +626,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
params,
|
params,
|
||||||
properties,
|
properties,
|
||||||
}}
|
}}
|
||||||
|
disableAddIssueOption={isArchivedIssues}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./all-lists";
|
export * from "./all-lists";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
export * from "./single-list";
|
export * from "./single-list";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -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-3 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 border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10"
|
||||||
|
{...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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -51,6 +51,7 @@ import {
|
|||||||
type Props = {
|
type Props = {
|
||||||
type?: string;
|
type?: string;
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
groupTitle?: string;
|
groupTitle?: string;
|
||||||
editIssue: () => void;
|
editIssue: () => void;
|
||||||
index: number;
|
index: number;
|
||||||
@ -69,6 +70,7 @@ type Props = {
|
|||||||
export const SingleListIssue: React.FC<Props> = ({
|
export const SingleListIssue: React.FC<Props> = ({
|
||||||
type,
|
type,
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
editIssue,
|
editIssue,
|
||||||
index,
|
index,
|
||||||
makeIssueCopy,
|
makeIssueCopy,
|
||||||
@ -88,7 +90,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
const [contextMenuPosition, setContextMenuPosition] = useState<React.MouseEvent | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
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 isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
const isDraftIssues = router.pathname?.split("/")?.[4] === "draft-issues";
|
||||||
|
|
||||||
@ -326,7 +328,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b border-custom-border-200 bg-custom-background-100 last:border-b-0"
|
className="flex items-center justify-between px-4 py-2.5 gap-10 border-b-[0.5px] border-custom-border-100 bg-custom-background-100 last:border-b-0"
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu(true);
|
setContextMenu(true);
|
||||||
@ -350,6 +352,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
className="truncate text-[0.825rem] text-custom-text-100"
|
className="truncate text-[0.825rem] text-custom-text-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (isArchivedIssues) return router.push(issuePath);
|
||||||
if (!isDraftIssues) openPeekOverview(issue);
|
if (!isDraftIssues) openPeekOverview(issue);
|
||||||
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
if (isDraftIssues && handleDraftIssueSelect) handleDraftIssueSelect(issue);
|
||||||
}}
|
}}
|
||||||
@ -376,6 +379,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
{properties.state && (
|
{properties.state && (
|
||||||
<StateSelect
|
<StateSelect
|
||||||
value={issue.state_detail}
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleStateChange}
|
onChange={handleStateChange}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed}
|
||||||
@ -400,6 +404,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
{properties.labels && (
|
{properties.labels && (
|
||||||
<LabelSelect
|
<LabelSelect
|
||||||
value={issue.labels}
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleLabelChange}
|
onChange={handleLabelChange}
|
||||||
labelsDetails={issue.label_details}
|
labelsDetails={issue.label_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
@ -411,6 +416,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
{properties.assignee && (
|
{properties.assignee && (
|
||||||
<MembersSelect
|
<MembersSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleAssigneeChange}
|
onChange={handleAssigneeChange}
|
||||||
membersDetails={issue.assignee_details}
|
membersDetails={issue.assignee_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
// components
|
// components
|
||||||
import { SingleListIssue } from "components/core";
|
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CustomMenu } from "components/ui";
|
import { Avatar, CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -31,7 +34,7 @@ import {
|
|||||||
UserAuth,
|
UserAuth,
|
||||||
} from "types";
|
} from "types";
|
||||||
// fetch-keys
|
// 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
|
// constants
|
||||||
import { STATE_GROUP_COLORS } from "constants/state";
|
import { STATE_GROUP_COLORS } from "constants/state";
|
||||||
|
|
||||||
@ -51,40 +54,65 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = (props) => {
|
||||||
currentState,
|
const {
|
||||||
groupTitle,
|
currentState,
|
||||||
addIssueToGroup,
|
groupTitle,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
handleDraftIssueAction,
|
handleDraftIssueAction,
|
||||||
handleMyIssueOpen,
|
handleMyIssueOpen,
|
||||||
removeIssue,
|
addIssueToGroup,
|
||||||
disableUserActions,
|
removeIssue,
|
||||||
disableAddIssueOption = false,
|
disableUserActions,
|
||||||
user,
|
disableAddIssueOption = false,
|
||||||
userAuth,
|
user,
|
||||||
viewProps,
|
userAuth,
|
||||||
}) => {
|
viewProps,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
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 isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
const { data: issueLabels } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
|
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||||
workspaceSlug && projectId
|
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
|
: 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
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
|
workspaceSlug &&
|
||||||
workspaceSlug && projectId
|
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)
|
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -99,7 +127,10 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
title = addSpaceIfCamelCase(currentState?.name ?? "");
|
||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
title = issueLabels?.find((label) => label.id === groupTitle)?.name ?? "None";
|
title =
|
||||||
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
@ -153,7 +184,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
break;
|
break;
|
||||||
case "labels":
|
case "labels":
|
||||||
const labelColor =
|
const labelColor =
|
||||||
issueLabels?.find((label) => label.id === groupTitle)?.color ?? "#000000";
|
[...(issueLabels ?? []), ...(workspaceLabels ?? [])]?.find(
|
||||||
|
(label) => label.id === groupTitle
|
||||||
|
)?.color ?? "#000000";
|
||||||
icon = (
|
icon = (
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 flex-shrink-0 rounded-full"
|
className="h-3 w-3 flex-shrink-0 rounded-full"
|
||||||
@ -206,8 +239,12 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
!disableAddIssueOption && (
|
!disableAddIssueOption && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToGroup}
|
onClick={() => {
|
||||||
|
if (isDraftIssuesPage || isMyIssuesPage || isProfileIssuesPage) {
|
||||||
|
addIssueToGroup();
|
||||||
|
} else setIsCreateIssueFormOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -224,7 +261,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
position="right"
|
position="right"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||||
|
Create new
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
Add an existing issue
|
Add an existing issue
|
||||||
@ -250,6 +289,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
key={issue.id}
|
key={issue.id}
|
||||||
type={type}
|
type={type}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
projectId={issue.project_detail.id}
|
||||||
groupTitle={groupTitle}
|
groupTitle={groupTitle}
|
||||||
index={index}
|
index={index}
|
||||||
editIssue={() => handleIssueAction(issue, "edit")}
|
editIssue={() => handleIssueAction(issue, "edit")}
|
||||||
@ -284,6 +324,34 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
<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 && (
|
||||||
|
// TODO: add border here
|
||||||
|
<div className="w-full bg-custom-background-100 px-6 py-3 border-b border-custom-border-100">
|
||||||
|
<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>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</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-100">
|
||||||
|
{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";
|
@ -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-100">
|
||||||
|
{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";
|
@ -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-100">
|
||||||
|
{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";
|
@ -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-100">
|
||||||
|
{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";
|
@ -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,13 @@
|
|||||||
|
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 "./spreadsheet-view";
|
||||||
export * from "./single-issue";
|
export * from "./issue-column/issue-column";
|
||||||
export * from "./spreadsheet-columns";
|
export * from "./issue-column/spreadsheet-issue-column";
|
||||||
export * from "./spreadsheet-issues";
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-issue-column";
|
||||||
|
export * from "./issue-column";
|
@ -0,0 +1,180 @@
|
|||||||
|
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-100">
|
||||||
|
{properties.key && (
|
||||||
|
<div
|
||||||
|
className="flex gap-1.5 px-4 pr-0 py-2.5 items-center min-w-[96px]"
|
||||||
|
style={issue.parent && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||||
|
>
|
||||||
|
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100">
|
||||||
|
<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-100 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,36 +1,34 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SingleSpreadsheetIssue } from "components/core";
|
import { IssueColumn } from "components/core";
|
||||||
// hooks
|
// hooks
|
||||||
import useSubIssue from "hooks/use-sub-issue";
|
import useSubIssue from "hooks/use-sub-issue";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import { IIssue, Properties, UserAuth } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
index: number;
|
projectId: string;
|
||||||
expandedIssues: string[];
|
expandedIssues: string[];
|
||||||
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
setExpandedIssues: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
gridTemplateColumns: string;
|
setCurrentProjectId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
nestingLevel?: number;
|
nestingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetIssues: React.FC<Props> = ({
|
export const SpreadsheetIssuesColumn: React.FC<Props> = ({
|
||||||
index,
|
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
expandedIssues,
|
expandedIssues,
|
||||||
setExpandedIssues,
|
setExpandedIssues,
|
||||||
gridTemplateColumns,
|
|
||||||
properties,
|
properties,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
|
setCurrentProjectId,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
user,
|
|
||||||
userAuth,
|
userAuth,
|
||||||
nestingLevel = 0,
|
nestingLevel = 0,
|
||||||
}) => {
|
}) => {
|
||||||
@ -49,21 +47,20 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isExpanded = expandedIssues.indexOf(issue.id) > -1;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SingleSpreadsheetIssue
|
<IssueColumn
|
||||||
issue={issue}
|
issue={issue}
|
||||||
index={index}
|
projectId={projectId}
|
||||||
expanded={isExpanded}
|
expanded={isExpanded}
|
||||||
handleToggleExpand={handleToggleExpand}
|
handleToggleExpand={handleToggleExpand}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
handleEditIssue={() => handleIssueAction(issue, "edit")}
|
||||||
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
handleDeleteIssue={() => handleIssueAction(issue, "delete")}
|
||||||
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel}
|
nestingLevel={nestingLevel}
|
||||||
/>
|
/>
|
||||||
@ -73,17 +70,16 @@ export const SpreadsheetIssues: React.FC<Props> = ({
|
|||||||
subIssues &&
|
subIssues &&
|
||||||
subIssues.length > 0 &&
|
subIssues.length > 0 &&
|
||||||
subIssues.map((subIssue: IIssue) => (
|
subIssues.map((subIssue: IIssue) => (
|
||||||
<SpreadsheetIssues
|
<SpreadsheetIssuesColumn
|
||||||
key={subIssue.id}
|
key={subIssue.id}
|
||||||
issue={subIssue}
|
issue={subIssue}
|
||||||
index={index}
|
projectId={subIssue.project_detail.id}
|
||||||
expandedIssues={expandedIssues}
|
expandedIssues={expandedIssues}
|
||||||
setExpandedIssues={setExpandedIssues}
|
setExpandedIssues={setExpandedIssues}
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
|
||||||
properties={properties}
|
properties={properties}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
|
setCurrentProjectId={setCurrentProjectId}
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
user={user}
|
|
||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
nestingLevel={nestingLevel + 1}
|
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-100">
|
||||||
|
{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-100">
|
||||||
|
{properties.priority && (
|
||||||
|
<PrioritySelect
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={handlePriorityChange}
|
||||||
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -49,6 +49,7 @@ import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: IIssue;
|
issue: IIssue;
|
||||||
|
projectId: string;
|
||||||
index: number;
|
index: number;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
handleToggleExpand: (issueId: string) => void;
|
handleToggleExpand: (issueId: string) => void;
|
||||||
@ -64,6 +65,7 @@ type Props = {
|
|||||||
|
|
||||||
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||||
issue,
|
issue,
|
||||||
|
projectId,
|
||||||
index,
|
index,
|
||||||
expanded,
|
expanded,
|
||||||
handleToggleExpand,
|
handleToggleExpand,
|
||||||
@ -80,7 +82,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
const { params } = useSpreadsheetIssuesView();
|
const { params } = useSpreadsheetIssuesView();
|
||||||
|
|
||||||
@ -96,7 +98,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
|
||||||
: viewId
|
: viewId
|
||||||
? VIEW_ISSUES(viewId.toString(), params)
|
? VIEW_ISSUES(viewId.toString(), params)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId, params);
|
||||||
|
|
||||||
if (issue.parent)
|
if (issue.parent)
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
@ -136,13 +138,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(
|
.patchIssue(workspaceSlug as string, projectId, issue.id as string, formData, user)
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
issue.id as string,
|
|
||||||
formData,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (issue.parent) {
|
if (issue.parent) {
|
||||||
mutate(SUB_ISSUES(issue.parent as string));
|
mutate(SUB_ISSUES(issue.parent as string));
|
||||||
@ -368,6 +364,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
<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">
|
<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">
|
||||||
<StateSelect
|
<StateSelect
|
||||||
value={issue.state_detail}
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleStateChange}
|
onChange={handleStateChange}
|
||||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
@ -390,6 +387,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
<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">
|
<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">
|
||||||
<MembersSelect
|
<MembersSelect
|
||||||
value={issue.assignees}
|
value={issue.assignees}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleAssigneeChange}
|
onChange={handleAssigneeChange}
|
||||||
membersDetails={issue.assignee_details}
|
membersDetails={issue.assignee_details}
|
||||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||||
@ -402,6 +400,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
<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">
|
<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">
|
||||||
<LabelSelect
|
<LabelSelect
|
||||||
value={issue.labels}
|
value={issue.labels}
|
||||||
|
projectId={projectId}
|
||||||
onChange={handleLabelChange}
|
onChange={handleLabelChange}
|
||||||
labelsDetails={issue.label_details}
|
labelsDetails={issue.label_details}
|
||||||
hideDropdownArrow
|
hideDropdownArrow
|
||||||
|
@ -1,274 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
// hooks
|
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
|
||||||
// component
|
|
||||||
import { CustomMenu, Icon } from "components/ui";
|
|
||||||
// icon
|
|
||||||
import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
import { TIssueOrderByOptions } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
columnData: any;
|
|
||||||
gridTemplateColumns: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateColumns }) => {
|
|
||||||
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
|
||||||
"spreadsheetViewSorting",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
|
||||||
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
|
||||||
|
|
||||||
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
|
|
||||||
|
|
||||||
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
|
||||||
setDisplayFilters({ order_by: order });
|
|
||||||
setSelectedMenuItem(`${order}_${itemKey}`);
|
|
||||||
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`grid auto-rows-[minmax(36px,1fr)] w-full min-w-max`}
|
|
||||||
style={{ gridTemplateColumns }}
|
|
||||||
>
|
|
||||||
{columnData.map((col: any) => {
|
|
||||||
if (col.isActive) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`bg-custom-background-90 w-full ${
|
|
||||||
col.propertyName === "title"
|
|
||||||
? "sticky left-0 z-20 bg-custom-background-90 pl-24"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{col.propertyName === "title" ? (
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-start gap-1.5 cursor-default text-sm text-custom-text-200 text-current w-full py-2.5 px-2`}
|
|
||||||
>
|
|
||||||
{col.colName}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<CustomMenu
|
|
||||||
className="!w-full"
|
|
||||||
customButton={
|
|
||||||
<div
|
|
||||||
className={`relative group flex items-center justify-start gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
|
||||||
activeSortingProperty === col.propertyName ? "bg-custom-background-80" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{activeSortingProperty === col.propertyName && (
|
|
||||||
<div className="absolute top-1 right-1.5">
|
|
||||||
<Icon
|
|
||||||
iconName="filter_list"
|
|
||||||
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{col.icon ? (
|
|
||||||
<col.icon
|
|
||||||
className={`text-custom-text-200 group-hover:text-custom-text-100 ${
|
|
||||||
col.propertyName === "estimate" ? "-rotate-90" : ""
|
|
||||||
}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
height="14"
|
|
||||||
width="14"
|
|
||||||
/>
|
|
||||||
) : col.propertyName === "priority" ? (
|
|
||||||
<span className="text-sm material-symbols-rounded text-custom-text-200">
|
|
||||||
signal_cellular_alt
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
|
|
||||||
{col.colName}
|
|
||||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width="xl"
|
|
||||||
>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy(col.ascendingOrder, col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
|
||||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>A</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Z</span>
|
|
||||||
</>
|
|
||||||
) : col.propertyName === "due_date" ||
|
|
||||||
col.propertyName === "created_on" ||
|
|
||||||
col.propertyName === "updated_on" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>New</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Old</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
|
||||||
</span>
|
|
||||||
<span>First</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>Last</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
|
||||||
selectedMenuItem === `${col.ascendingOrder}_${col.propertyName}`
|
|
||||||
? "opacity-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className={`mt-0.5 ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "bg-custom-background-80"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
key={col.property}
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy(col.descendingOrder, col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "text-custom-text-100"
|
|
||||||
: "text-custom-text-200 hover:text-custom-text-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{col.propertyName === "assignee" || col.propertyName === "labels" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Z</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>A</span>
|
|
||||||
</>
|
|
||||||
) : col.propertyName === "due_date" ? (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Old</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>New</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="relative flex items-center h-6 w-6">
|
|
||||||
<Icon
|
|
||||||
iconName="east"
|
|
||||||
className="absolute left-0 -rotate-90 text-xs leading-3"
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
iconName="sort"
|
|
||||||
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>Last</span>
|
|
||||||
<Icon iconName="east" className="text-sm" />
|
|
||||||
<span>First</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckIcon
|
|
||||||
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
|
||||||
selectedMenuItem === `${col.descendingOrder}_${col.propertyName}`
|
|
||||||
? "opacity-100"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
{selectedMenuItem &&
|
|
||||||
selectedMenuItem !== "" &&
|
|
||||||
displayFilters?.order_by !== "-created_at" &&
|
|
||||||
selectedMenuItem.includes(col.propertyName) && (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
className={`mt-0.5${
|
|
||||||
selectedMenuItem === `-created_at_${col.propertyName}`
|
|
||||||
? "bg-custom-background-80"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
key={col.property}
|
|
||||||
onClick={() => {
|
|
||||||
handleOrderBy("-created_at", col.propertyName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
|
|
||||||
<div className="flex gap-1.5 items-center">
|
|
||||||
<span className="relative flex items-center justify-center h-6 w-6">
|
|
||||||
<Icon iconName="ink_eraser" className="text-sm" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>Clear sorting</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
)}
|
|
||||||
</CustomMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,23 +1,61 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
// next
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { KeyedMutator, mutate } from "swr";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
import {
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
ListInlineCreateIssueForm,
|
||||||
|
SpreadsheetAssigneeColumn,
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
SpreadsheetIssuesColumn,
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
} from "components/core";
|
||||||
|
import { CustomMenu, Icon, Spinner } from "components/ui";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
import useIssuesProperties from "hooks/use-issue-properties";
|
import useIssuesProperties from "hooks/use-issue-properties";
|
||||||
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
|
import { useWorkspaceView } from "hooks/use-workspace-view";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types";
|
import {
|
||||||
// constants
|
ICurrentUserResponse,
|
||||||
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
|
IIssue,
|
||||||
|
ISubIssueResponse,
|
||||||
|
TIssueOrderByOptions,
|
||||||
|
UserAuth,
|
||||||
|
} from "types";
|
||||||
|
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
|
// icon
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, ChevronDownIcon, PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
spreadsheetIssues: IIssue[];
|
||||||
|
mutateIssues: KeyedMutator<
|
||||||
|
| IIssue[]
|
||||||
|
| {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void;
|
||||||
openIssuesListModal?: (() => void) | null;
|
openIssuesListModal?: (() => void) | null;
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
@ -26,6 +64,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetView: React.FC<Props> = ({
|
export const SpreadsheetView: React.FC<Props> = ({
|
||||||
|
spreadsheetIssues,
|
||||||
|
mutateIssues,
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
disableUserActions,
|
disableUserActions,
|
||||||
@ -33,99 +73,588 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId, globalViewId } = router.query;
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
|
|
||||||
const { spreadsheetIssues, mutateIssues } = useSpreadsheetIssuesView();
|
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
...column,
|
"spreadsheetViewSorting",
|
||||||
isActive: properties
|
""
|
||||||
? column.propertyName === "labels"
|
);
|
||||||
? properties[column.propertyName as keyof Properties]
|
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
|
||||||
: column.propertyName === "title"
|
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
|
||||||
? true
|
|
||||||
: properties[column.propertyName as keyof Properties]
|
|
||||||
: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gridTemplateColumns = columnData
|
const workspaceIssuesPath = [
|
||||||
.filter((column) => column.isActive)
|
{
|
||||||
.map((column) => column.colSize)
|
params: {
|
||||||
.join(" ");
|
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 currentWorkspaceIssuePath = workspaceIssuesPath.find((path) =>
|
||||||
|
router.pathname.includes(path.path)
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
params: workspaceViewParams,
|
||||||
|
filters: workspaceViewFilters,
|
||||||
|
handleFilters,
|
||||||
|
} = useWorkspaceView();
|
||||||
|
|
||||||
|
const workspaceViewProperties = workspaceViewFilters.display_properties;
|
||||||
|
|
||||||
|
const isWorkspaceView = globalViewId || currentWorkspaceIssuePath;
|
||||||
|
|
||||||
|
const currentViewProperties = isWorkspaceView ? workspaceViewProperties : properties;
|
||||||
|
|
||||||
|
const { params, displayFilters, setDisplayFilters } = 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)
|
||||||
|
: globalViewId
|
||||||
|
? WORKSPACE_VIEW_ISSUES(globalViewId.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,
|
||||||
|
globalViewId,
|
||||||
|
workspaceViewParams,
|
||||||
|
currentWorkspaceIssuePath,
|
||||||
|
params,
|
||||||
|
user,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
|
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
|
||||||
|
if (globalViewId) handleFilters("display_filters", { order_by: order });
|
||||||
|
else setDisplayFilters({ order_by: order });
|
||||||
|
setSelectedMenuItem(`${order}_${itemKey}`);
|
||||||
|
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderColumn = (
|
||||||
|
header: string,
|
||||||
|
propertyName: string,
|
||||||
|
Component: React.ComponentType<any>,
|
||||||
|
ascendingOrder: TIssueOrderByOptions,
|
||||||
|
descendingOrder: TIssueOrderByOptions
|
||||||
|
) => (
|
||||||
|
<div className="relative flex flex-col h-max w-full bg-custom-background-100">
|
||||||
|
<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-100">
|
||||||
|
{currentWorkspaceIssuePath ? (
|
||||||
|
<span>{header}</span>
|
||||||
|
) : (
|
||||||
|
<CustomMenu
|
||||||
|
customButtonClassName="!w-full"
|
||||||
|
className="!w-full"
|
||||||
|
position="left"
|
||||||
|
customButton={
|
||||||
|
<div
|
||||||
|
className={`relative group flex items-center justify-between gap-1.5 cursor-pointer text-sm text-custom-text-200 hover:text-custom-text-100 w-full py-3 px-2 ${
|
||||||
|
activeSortingProperty === propertyName ? "bg-custom-background-80" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{activeSortingProperty === propertyName && (
|
||||||
|
<div className="absolute top-1 right-1.5">
|
||||||
|
<Icon
|
||||||
|
iconName="filter_list"
|
||||||
|
className="flex items-center justify-center h-3.5 w-3.5 rounded-full bg-custom-primary text-xs text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{header}
|
||||||
|
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="xl"
|
||||||
|
>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(ascendingOrder, propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${ascendingOrder}_${propertyName}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{propertyName === "assignee" || propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>A</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Z</span>
|
||||||
|
</>
|
||||||
|
) : propertyName === "due_date" ||
|
||||||
|
propertyName === "created_on" ||
|
||||||
|
propertyName === "updated_on" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>New</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Old</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon iconName="sort" className="absolute right-0 text-sm" />
|
||||||
|
</span>
|
||||||
|
<span>First</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>Last</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${ascendingOrder}_${propertyName}` ? "opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`mt-0.5 ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}`
|
||||||
|
? "bg-custom-background-80"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={propertyName}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy(descendingOrder, propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`group flex gap-1.5 px-1 items-center justify-between ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}`
|
||||||
|
? "text-custom-text-100"
|
||||||
|
: "text-custom-text-200 hover:text-custom-text-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{propertyName === "assignee" || propertyName === "labels" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Z</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>A</span>
|
||||||
|
</>
|
||||||
|
) : propertyName === "due_date" ? (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Old</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>New</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="relative flex items-center h-6 w-6">
|
||||||
|
<Icon
|
||||||
|
iconName="east"
|
||||||
|
className="absolute left-0 -rotate-90 text-xs leading-3"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
iconName="sort"
|
||||||
|
className="absolute rotate-180 transform scale-x-[-1] right-0 text-sm"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Last</span>
|
||||||
|
<Icon iconName="east" className="text-sm" />
|
||||||
|
<span>First</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CheckIcon
|
||||||
|
className={`h-3.5 w-3.5 opacity-0 group-hover:opacity-100 ${
|
||||||
|
selectedMenuItem === `${descendingOrder}_${propertyName}` ? "opacity-100" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
{selectedMenuItem &&
|
||||||
|
selectedMenuItem !== "" &&
|
||||||
|
displayFilters?.order_by !== "-created_at" &&
|
||||||
|
selectedMenuItem.includes(propertyName) && (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
className={`mt-0.5${
|
||||||
|
selectedMenuItem === `-created_at_${propertyName}`
|
||||||
|
? "bg-custom-background-80"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
key={propertyName}
|
||||||
|
onClick={() => {
|
||||||
|
handleOrderBy("-created_at", propertyName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`group flex gap-1.5 px-1 items-center justify-between `}>
|
||||||
|
<div className="flex gap-1.5 items-center">
|
||||||
|
<span className="relative flex items-center justify-center h-6 w-6">
|
||||||
|
<Icon iconName="ink_eraser" className="text-sm" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>Clear sorting</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
)}
|
||||||
|
</CustomMenu>
|
||||||
|
)}
|
||||||
|
</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={currentViewProperties}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const scrollLeft = containerRef.current.scrollLeft;
|
||||||
|
setIsScrolled(scrollLeft > 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentContainerRef = containerRef.current;
|
||||||
|
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentContainerRef) {
|
||||||
|
currentContainerRef.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssuePeekOverview
|
<IssuePeekOverview
|
||||||
handleMutation={() => mutateIssues()}
|
handleMutation={() => mutateIssues()}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={currentProjectId ?? ""}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
readOnly={disableUserActions}
|
readOnly={disableUserActions}
|
||||||
/>
|
/>
|
||||||
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
|
<div className="relative flex h-full w-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-200">
|
||||||
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
|
<div className="h-full w-full flex flex-col">
|
||||||
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
|
<div ref={containerRef} className="flex max-h-full h-full overflow-y-auto">
|
||||||
</div>
|
{spreadsheetIssues ? (
|
||||||
{spreadsheetIssues ? (
|
<>
|
||||||
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
|
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||||
{spreadsheetIssues.map((issue: IIssue, index) => (
|
<div
|
||||||
<SpreadsheetIssues
|
className="relative flex flex-col h-max w-full bg-custom-background-100 z-[2]"
|
||||||
key={`${issue.id}_${index}`}
|
style={{
|
||||||
index={index}
|
boxShadow: isScrolled ? "8px -9px 12px rgba(0, 0, 0, 0.15)" : "",
|
||||||
issue={issue}
|
}}
|
||||||
expandedIssues={expandedIssues}
|
>
|
||||||
setExpandedIssues={setExpandedIssues}
|
<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-100">
|
||||||
gridTemplateColumns={gridTemplateColumns}
|
{currentViewProperties.key && (
|
||||||
properties={properties}
|
<span className="flex items-center px-4 py-2.5 h-full w-24 flex-shrink-0">
|
||||||
handleIssueAction={handleIssueAction}
|
ID
|
||||||
disableUserActions={disableUserActions}
|
</span>
|
||||||
user={user}
|
)}
|
||||||
userAuth={userAuth}
|
<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={currentViewProperties}
|
||||||
|
handleIssueAction={handleIssueAction}
|
||||||
|
disableUserActions={disableUserActions}
|
||||||
|
userAuth={userAuth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{currentViewProperties.state &&
|
||||||
|
renderColumn(
|
||||||
|
"State",
|
||||||
|
"state",
|
||||||
|
SpreadsheetStateColumn,
|
||||||
|
"state__name",
|
||||||
|
"-state__name"
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentViewProperties.priority &&
|
||||||
|
renderColumn(
|
||||||
|
"Priority",
|
||||||
|
"priority",
|
||||||
|
SpreadsheetPriorityColumn,
|
||||||
|
"priority",
|
||||||
|
"-priority"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.assignee &&
|
||||||
|
renderColumn(
|
||||||
|
"Assignees",
|
||||||
|
"assignee",
|
||||||
|
SpreadsheetAssigneeColumn,
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.labels &&
|
||||||
|
renderColumn(
|
||||||
|
"Label",
|
||||||
|
"labels",
|
||||||
|
SpreadsheetLabelColumn,
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.start_date &&
|
||||||
|
renderColumn(
|
||||||
|
"Start Date",
|
||||||
|
"start_date",
|
||||||
|
SpreadsheetStartDateColumn,
|
||||||
|
"-start_date",
|
||||||
|
"start_date"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.due_date &&
|
||||||
|
renderColumn(
|
||||||
|
"Due Date",
|
||||||
|
"due_date",
|
||||||
|
SpreadsheetDueDateColumn,
|
||||||
|
"-target_date",
|
||||||
|
"target_date"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.estimate &&
|
||||||
|
renderColumn(
|
||||||
|
"Estimate",
|
||||||
|
"estimate",
|
||||||
|
SpreadsheetEstimateColumn,
|
||||||
|
"estimate_point",
|
||||||
|
"-estimate_point"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.created_on &&
|
||||||
|
renderColumn(
|
||||||
|
"Created On",
|
||||||
|
"created_on",
|
||||||
|
SpreadsheetCreatedOnColumn,
|
||||||
|
"-created_at",
|
||||||
|
"created_at"
|
||||||
|
)}
|
||||||
|
{currentViewProperties.updated_on &&
|
||||||
|
renderColumn(
|
||||||
|
"Updated On",
|
||||||
|
"updated_on",
|
||||||
|
SpreadsheetUpdatedOnColumn,
|
||||||
|
"-updated_at",
|
||||||
|
"updated_at"
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col justify-center items-center h-full w-full">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-custom-border-100">
|
||||||
|
<div className="mb-3 z-50 sticky bottom-0 left-0">
|
||||||
|
<ListInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
</div>
|
||||||
<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"
|
{type === "issue"
|
||||||
style={{ gridTemplateColumns }}
|
? !disableUserActions &&
|
||||||
>
|
!isInlineCreateIssueFormOpen && (
|
||||||
{type === "issue" ? (
|
<button
|
||||||
<button
|
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] w-full"
|
||||||
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={() => setIsInlineCreateIssueFormOpen(true)}
|
||||||
onClick={() => {
|
>
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
<PlusIcon className="h-4 w-4" />
|
||||||
document.dispatchEvent(e);
|
New Issue
|
||||||
}}
|
</button>
|
||||||
>
|
)
|
||||||
<PlusIcon className="h-4 w-4" />
|
: !disableUserActions &&
|
||||||
Add Issue
|
!isInlineCreateIssueFormOpen && (
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
!disableUserActions && (
|
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
className="sticky left-0 z-[1]"
|
className="sticky left-0 z-10"
|
||||||
customButton={
|
customButton={
|
||||||
<button
|
<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"
|
className="flex gap-1.5 items-center text-custom-primary-100 pl-4 py-2.5 text-sm sticky left-0 z-[1] border-custom-border-200 w-full"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
New Issue
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
position="left"
|
position="left"
|
||||||
|
verticalPosition="top"
|
||||||
optionsClassName="left-5 !w-36"
|
optionsClassName="left-5 !w-36"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
@ -134,13 +663,9 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-start-date-column";
|
||||||
|
export * from "./start-date-column";
|
@ -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-100">
|
||||||
|
{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-100">
|
||||||
|
{properties.state && (
|
||||||
|
<StateSelect
|
||||||
|
value={issue.state_detail}
|
||||||
|
projectId={projectId}
|
||||||
|
onChange={handleStateChange}
|
||||||
|
buttonClassName="!shadow-none !border-0"
|
||||||
|
hideDropdownArrow
|
||||||
|
disabled={isNotAllowed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./spreadsheet-updated-on-column";
|
||||||
|
export * from "./updated-on-column";
|
@ -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-100">
|
||||||
|
{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>
|
||||||
|
);
|
@ -1,14 +1,15 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// mobx
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// ui
|
// ui
|
||||||
import { CustomSearchSelect } from "components/ui";
|
import { CustomSearchSelect } from "components/ui";
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
// icons
|
||||||
import { TableProperties } from "lucide-react";
|
import { TableProperties } from "lucide-react";
|
||||||
|
// helpers
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChange: (val: string | null) => void;
|
onChange: (val: string | null) => void;
|
||||||
|
@ -149,6 +149,10 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
|||||||
color: group.color,
|
color: group.color,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const completedIssues = cycle.completed_issues + cycle.cancelled_issues;
|
||||||
|
|
||||||
|
const percentage = cycle.total_issues > 0 ? (completedIssues / cycle.total_issues) * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col text-xs hover:bg-custom-background-80">
|
<div className="flex flex-col text-xs hover:bg-custom-background-80">
|
||||||
@ -307,7 +311,7 @@ export const SingleCycleList: React.FC<TSingleStatProps> = ({
|
|||||||
) : cycleStatus === "completed" ? (
|
) : cycleStatus === "completed" ? (
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
<RadialProgressBar progress={100} />
|
<RadialProgressBar progress={100} />
|
||||||
<span>{100} %</span>
|
<span>{Math.round(percentage)} %</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex gap-1">
|
<span className="flex gap-1">
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
|
import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/20/solid";
|
||||||
// components
|
// components
|
||||||
@ -11,6 +13,8 @@ import { GanttSidebar } from "../sidebar";
|
|||||||
import { MonthChartView } from "./month";
|
import { MonthChartView } from "./month";
|
||||||
// import { QuarterChartView } from "./quarter";
|
// import { QuarterChartView } from "./quarter";
|
||||||
// import { YearChartView } from "./year";
|
// import { YearChartView } from "./year";
|
||||||
|
// icons
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
// views
|
// views
|
||||||
import {
|
import {
|
||||||
// generateHourChart,
|
// generateHourChart,
|
||||||
@ -25,6 +29,7 @@ import {
|
|||||||
getNumberOfDaysBetweenTwoDatesInYear,
|
getNumberOfDaysBetweenTwoDatesInYear,
|
||||||
getMonthChartItemPositionWidthInMonth,
|
getMonthChartItemPositionWidthInMonth,
|
||||||
} from "../views";
|
} from "../views";
|
||||||
|
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
|
||||||
// types
|
// types
|
||||||
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types";
|
||||||
// data
|
// data
|
||||||
@ -64,12 +69,17 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
||||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
// blocks state management starts
|
// blocks state management starts
|
||||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null);
|
||||||
|
|
||||||
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
|
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } =
|
||||||
useChart();
|
useChart();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
? blocks.map((block: any) => ({
|
? blocks.map((block: any) => ({
|
||||||
@ -294,9 +304,12 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="gantt-sidebar"
|
id="gantt-sidebar"
|
||||||
className="h-full w-1/4 flex flex-col border-r border-custom-border-200 space-y-3"
|
className="h-full w-1/4 flex flex-col border-r border-custom-border-200"
|
||||||
>
|
>
|
||||||
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0" />
|
<div className="h-[60px] border-b border-custom-border-200 box-border flex-shrink-0 flex items-end justify-between gap-2 text-sm text-custom-text-300 font-medium pb-2 pl-10 pr-4">
|
||||||
|
<h6>{title}</h6>
|
||||||
|
<h6>Duration</h6>
|
||||||
|
</div>
|
||||||
<GanttSidebar
|
<GanttSidebar
|
||||||
title={title}
|
title={title}
|
||||||
blockUpdateHandler={blockUpdateHandler}
|
blockUpdateHandler={blockUpdateHandler}
|
||||||
@ -304,6 +317,44 @@ export const ChartViewRoot: FC<ChartViewRootProps> = ({
|
|||||||
SidebarBlockRender={SidebarBlockRender}
|
SidebarBlockRender={SidebarBlockRender}
|
||||||
enableReorder={enableReorder}
|
enableReorder={enableReorder}
|
||||||
/>
|
/>
|
||||||
|
{chartBlocks && (
|
||||||
|
<div className="pl-2.5 py-3">
|
||||||
|
<GanttInlineCreateIssueForm
|
||||||
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (ganttSidebar)
|
||||||
|
ganttSidebar.scrollBy({
|
||||||
|
top: ganttSidebar.scrollHeight,
|
||||||
|
left: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}, 10);
|
||||||
|
}}
|
||||||
|
prePopulatedData={{
|
||||||
|
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
||||||
|
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isCreateIssueFormOpen && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
|
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 pl-[1.875rem] 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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
|
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto horizontal-scroll-enable"
|
||||||
|
@ -179,11 +179,6 @@ export const ChartDraggable: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
setIsMoving(true);
|
|
||||||
|
|
||||||
const resizableDiv = resizableRef.current;
|
const resizableDiv = resizableRef.current;
|
||||||
|
|
||||||
const columnWidth = currentViewData.data.width;
|
const columnWidth = currentViewData.data.width;
|
||||||
@ -193,6 +188,8 @@ export const ChartDraggable: React.FC<Props> = ({
|
|||||||
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
let initialMarginLeft = parseInt(resizableDiv.style.marginLeft);
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
setIsMoving(true);
|
||||||
|
|
||||||
let delWidth = 0;
|
let delWidth = 0;
|
||||||
|
|
||||||
delWidth = checkScrollEnd(e);
|
delWidth = checkScrollEnd(e);
|
||||||
@ -295,7 +292,9 @@ export const ChartDraggable: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="relative z-[2] rounded h-8 w-full flex items-center"
|
className={`relative z-[2] rounded h-8 w-full flex items-center ${
|
||||||
|
isMoving ? "pointer-events-none" : ""
|
||||||
|
}`}
|
||||||
onMouseDown={handleBlockMove}
|
onMouseDown={handleBlockMove}
|
||||||
>
|
>
|
||||||
<BlockRender data={block.data} />
|
<BlockRender data={block.data} />
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
@ -7,6 +8,8 @@ import { useChart } from "./hooks";
|
|||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||||
|
// helpers
|
||||||
|
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||||
|
|
||||||
@ -18,13 +21,12 @@ type Props = {
|
|||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttSidebar: React.FC<Props> = ({
|
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||||
title,
|
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||||
blockUpdateHandler,
|
|
||||||
blocks,
|
const router = useRouter();
|
||||||
SidebarBlockRender,
|
const { cycleId } = router.query;
|
||||||
enableReorder,
|
|
||||||
}) => {
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
@ -85,14 +87,21 @@ export const GanttSidebar: React.FC<Props> = ({
|
|||||||
<StrictModeDroppable droppableId="gantt-sidebar">
|
<StrictModeDroppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div
|
||||||
className="h-full overflow-y-auto pl-2.5"
|
id={`gantt-sidebar-${cycleId}`}
|
||||||
|
className="max-h-full overflow-y-auto pl-2.5 mt-3"
|
||||||
ref={droppableProvided.innerRef}
|
ref={droppableProvided.innerRef}
|
||||||
{...droppableProvided.droppableProps}
|
{...droppableProvided.droppableProps}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{blocks ? (
|
{blocks ? (
|
||||||
blocks.length > 0 ? (
|
blocks.map((block, index) => {
|
||||||
blocks.map((block, index) => (
|
const duration = findTotalDaysInRange(
|
||||||
|
block.start_date ?? "",
|
||||||
|
block.target_date ?? "",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={`sidebar-block-${block.id}`}
|
key={`sidebar-block-${block.id}`}
|
||||||
draggableId={`sidebar-block-${block.id}`}
|
draggableId={`sidebar-block-${block.id}`}
|
||||||
@ -125,19 +134,20 @@ export const GanttSidebar: React.FC<Props> = ({
|
|||||||
<EllipsisVerticalIcon className="h-4 -ml-5" />
|
<EllipsisVerticalIcon className="h-4 -ml-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="flex-grow truncate w-full h-full">
|
<div className="flex-grow truncate h-full flex items-center justify-between gap-2">
|
||||||
<SidebarBlockRender data={block.data} />
|
<div className="flex-grow truncate">
|
||||||
|
<SidebarBlockRender data={block.data} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 text-sm text-custom-text-200">
|
||||||
|
{duration} day{duration > 1 ? "s" : ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))
|
);
|
||||||
) : (
|
})
|
||||||
<div className="text-custom-text-200 text-sm text-center mt-8">
|
|
||||||
No <span className="lowercase">{title}</span> found
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Loader className="pr-2 space-y-3">
|
<Loader className="pr-2 space-y-3">
|
||||||
<Loader.Item height="34px" />
|
<Loader.Item height="34px" />
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user