forked from github/plane
Merge branch 'develop' of github.com:makeplane/plane into feat/issue_relation
This commit is contained in:
commit
d0bd21eb5b
68
.github/workflows/push-image-backend.yml
vendored
68
.github/workflows/push-image-backend.yml
vendored
@ -1,12 +1,11 @@
|
|||||||
name: Build and Push Backend Docker Image
|
name: Build and Push Backend Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
branches:
|
types: [closed]
|
||||||
- 'develop'
|
branches: [develop]
|
||||||
- 'master'
|
release:
|
||||||
tags:
|
types: [released]
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_push_backend:
|
build_push_backend:
|
||||||
@ -17,61 +16,42 @@ jobs:
|
|||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3.3.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
|
||||||
with:
|
|
||||||
platforms: linux/arm64,linux/amd64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v2.1.0
|
|
||||||
with:
|
|
||||||
registry: "ghcr.io"
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
registry: "registry.hub.docker.com"
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
|
||||||
id: ghmeta
|
id: meta_pr
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
with:
|
with:
|
||||||
images: makeplane/plane-backend
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||||
|
tags: type=raw,value=develop
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: dkrmeta
|
id: meta
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository }}-backend
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||||
|
tags: |
|
||||||
- name: Build and Push to GitHub Container Registry
|
type=ref,event=tag
|
||||||
uses: docker/build-push-action@v4.0.0
|
|
||||||
with:
|
|
||||||
context: ./apiserver
|
|
||||||
file: ./apiserver/Dockerfile.api
|
|
||||||
platforms: linux/arm64,linux/amd64
|
|
||||||
push: true
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha
|
|
||||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
|
||||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Build and Push to Docker Hub
|
- name: Build and Push to Docker Hub
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: ./apiserver
|
context: ./apiserver
|
||||||
file: ./apiserver/Dockerfile.api
|
file: ./apiserver/Dockerfile.api
|
||||||
platforms: linux/arm64,linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
|
||||||
cache-to: type=gha
|
env:
|
||||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
DOCKER_BUILDKIT: 1
|
||||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
57
.github/workflows/push-image-deploy.yml
vendored
Normal file
57
.github/workflows/push-image-deploy.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: Build and Push Plane Deploy Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches: [develop]
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_push_plane_deploy:
|
||||||
|
name: Build and Push Plane Deploy Docker Image
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
|
||||||
|
id: meta_pr
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||||
|
tags: type=raw,value=develop
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Build and Push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./apps/space/Dockerfile.space
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
71
.github/workflows/push-image-frontend.yml
vendored
71
.github/workflows/push-image-frontend.yml
vendored
@ -1,77 +1,60 @@
|
|||||||
name: Build and Push Frontend Docker Image
|
name: Build and Push Frontend Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
branches:
|
types: [closed]
|
||||||
- 'develop'
|
branches: [develop]
|
||||||
- 'master'
|
release:
|
||||||
tags:
|
types: [released]
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_push_frontend:
|
build_push_frontend:
|
||||||
name: Build Frontend Docker Image
|
name: Build Frontend Docker Image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3.3.0
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
|
||||||
with:
|
|
||||||
platforms: linux/arm64,linux/amd64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2.5.0
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
- name: Login to Github Container Registry
|
- name: Setup .npmrc for repository
|
||||||
uses: docker/login-action@v2.1.0
|
run: |
|
||||||
with:
|
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
|
||||||
registry: "ghcr.io"
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2.1.0
|
uses: docker/login-action@v2.1.0
|
||||||
with:
|
with:
|
||||||
registry: "registry.hub.docker.com"
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
|
||||||
id: ghmeta
|
|
||||||
uses: docker/metadata-action@v4.3.0
|
|
||||||
with:
|
|
||||||
images: makeplane/plane-frontend
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
id: meta
|
id: meta
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
uses: docker/metadata-action@v4.3.0
|
uses: docker/metadata-action@v4.3.0
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository }}-frontend
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
- name: Build and Push to GitHub Container Registry
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
|
||||||
uses: docker/build-push-action@v4.0.0
|
id: meta_pr
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||||
file: ./apps/app/Dockerfile.web
|
tags: type=raw,value=develop
|
||||||
platforms: linux/arm64,linux/amd64
|
|
||||||
push: true
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha
|
|
||||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
|
||||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Build and Push to Docker Container Registry
|
- name: Build and Push to Docker Container Registry
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./apps/app/Dockerfile.web
|
file: ./apps/app/Dockerfile.web
|
||||||
platforms: linux/arm64,linux/amd64
|
platforms: linux/amd64
|
||||||
|
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
env:
|
||||||
cache-to: type=gha
|
DOCKER_BUILDKIT: 1
|
||||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
56
.github/workflows/push-image-proxy.yml
vendored
Normal file
56
.github/workflows/push-image-proxy.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
name: Build and Push Proxy Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches: [develop]
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_push_proxy:
|
||||||
|
name: Build and Push Proxy Docker Image
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out the repo
|
||||||
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.5.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2.1.0
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Pull Request
|
||||||
|
id: meta_pr
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||||
|
tags: type=raw,value=develop
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4.3.0
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
with:
|
||||||
|
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
|
||||||
|
- name: Build and Push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4.0.0
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ github.event_name == 'release' && steps.meta.outputs.tags || steps.meta_pr.outputs.tags }}
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
@ -112,7 +112,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectMemberSerializer(BaseSerializer):
|
class ProjectMemberSerializer(BaseSerializer):
|
||||||
workspace = WorkSpaceSerializer(read_only=True)
|
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||||
project = ProjectLiteSerializer(read_only=True)
|
project = ProjectLiteSerializer(read_only=True)
|
||||||
member = UserLiteSerializer(read_only=True)
|
member = UserLiteSerializer(read_only=True)
|
||||||
|
|
||||||
|
@ -287,7 +287,10 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
|
# Additional fields of the member
|
||||||
data["sort_order"] = project_member.sort_order
|
data["sort_order"] = project_member.sort_order
|
||||||
|
data["member_role"] = project_member.role
|
||||||
|
data["is_member"] = True
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors,
|
serializer.errors,
|
||||||
|
@ -1 +1 @@
|
|||||||
python-3.11.4
|
python-3.11.5
|
@ -53,7 +53,11 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
|
|||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
message: (activity: IIssueActivity, showIssue: boolean) => React.ReactNode;
|
message: (
|
||||||
|
activity: IIssueActivity,
|
||||||
|
showIssue: boolean,
|
||||||
|
workspaceSlug: string
|
||||||
|
) => React.ReactNode;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
@ -173,26 +177,50 @@ const activityDetails: {
|
|||||||
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
|
||||||
},
|
},
|
||||||
cycles: {
|
cycles: {
|
||||||
message: (activity) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added this issue to the cycle{" "}
|
added this issue to the cycle{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.new_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
else if (activity.verb === "updated")
|
else if (activity.verb === "updated")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the cycle to{" "}
|
set the cycle to{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.new_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.new_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the issue from the cycle{" "}
|
removed the issue from the cycle{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/cycles/${activity.old_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.old_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -351,26 +379,50 @@ const activityDetails: {
|
|||||||
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
message: (activity) => {
|
message: (activity, showIssue, workspaceSlug) => {
|
||||||
if (activity.verb === "created")
|
if (activity.verb === "created")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
added this issue to the module{" "}
|
added this issue to the module{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.new_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
else if (activity.verb === "updated")
|
else if (activity.verb === "updated")
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
set the module to{" "}
|
set the module to{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.new_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.new_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
removed the issue from the module{" "}
|
removed the issue from the module{" "}
|
||||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>.
|
<a
|
||||||
|
href={`/${workspaceSlug}/projects/${activity.project}/modules/${activity.old_identifier}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
|
||||||
|
>
|
||||||
|
{activity.old_value}
|
||||||
|
<Icon iconName="launch" className="!text-xs" />
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -538,8 +590,17 @@ export const ActivityMessage = ({
|
|||||||
}: {
|
}: {
|
||||||
activity: IIssueActivity;
|
activity: IIssueActivity;
|
||||||
showIssue?: boolean;
|
showIssue?: boolean;
|
||||||
}) => (
|
}) => {
|
||||||
<>
|
const router = useRouter();
|
||||||
{activityDetails[activity.field as keyof typeof activityDetails]?.message(activity, showIssue)}
|
const { workspaceSlug } = router.query;
|
||||||
</>
|
|
||||||
);
|
return (
|
||||||
|
<>
|
||||||
|
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
|
||||||
|
activity,
|
||||||
|
showIssue,
|
||||||
|
workspaceSlug?.toString() ?? ""
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -11,15 +11,18 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// hooks
|
// hooks
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// components
|
||||||
import { DueDateFilterSelect } from "./due-date-filter-select";
|
import { DateFilterSelect } from "./date-filter-select";
|
||||||
// ui
|
// ui
|
||||||
import { PrimaryButton, SecondaryButton } from "components/ui";
|
import { PrimaryButton, SecondaryButton } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderDateFormat, renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
field: keyof IIssueFilterOptions;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
};
|
};
|
||||||
@ -36,7 +39,7 @@ const defaultValues: TFormValues = {
|
|||||||
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) => {
|
export const DateFilterModal: React.FC<Props> = ({ title, field, isOpen, handleClose }) => {
|
||||||
const { filters, setFilters } = useIssuesView();
|
const { filters, setFilters } = useIssuesView();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -51,11 +54,11 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
|
|
||||||
if (filterType === "range") {
|
if (filterType === "range") {
|
||||||
setFilters(
|
setFilters(
|
||||||
{ target_date: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
{ [field]: [`${renderDateFormat(date1)};after`, `${renderDateFormat(date2)};before`] },
|
||||||
!Boolean(viewId)
|
!Boolean(viewId)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const filteredArray = filters?.target_date?.filter((item) => {
|
const filteredArray = (filters?.[field] as string[])?.filter((item) => {
|
||||||
if (item?.includes(filterType)) return false;
|
if (item?.includes(filterType)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -64,13 +67,13 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
const filterOne = filteredArray && filteredArray?.length > 0 ? filteredArray[0] : null;
|
||||||
if (filterOne)
|
if (filterOne)
|
||||||
setFilters(
|
setFilters(
|
||||||
{ target_date: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
{ [field]: [filterOne, `${renderDateFormat(date1)};${filterType}`] },
|
||||||
!Boolean(viewId)
|
!Boolean(viewId)
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
setFilters(
|
setFilters(
|
||||||
{
|
{
|
||||||
target_date: [`${renderDateFormat(date1)};${filterType}`],
|
[field]: [`${renderDateFormat(date1)};${filterType}`],
|
||||||
},
|
},
|
||||||
!Boolean(viewId)
|
!Boolean(viewId)
|
||||||
);
|
);
|
||||||
@ -116,7 +119,7 @@ export const DueDateFilterModal: React.FC<Props> = ({ isOpen, handleClose }) =>
|
|||||||
control={control}
|
control={control}
|
||||||
name="filterType"
|
name="filterType"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<DueDateFilterSelect value={value} onChange={onChange} />
|
<DateFilterSelect title={title} value={value} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
@ -7,6 +7,7 @@ import { CalendarBeforeIcon, CalendarAfterIcon, CalendarMonthIcon } from "compon
|
|||||||
// fetch-keys
|
// fetch-keys
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
@ -19,29 +20,31 @@ type DueDate = {
|
|||||||
|
|
||||||
const dueDateRange: DueDate[] = [
|
const dueDateRange: DueDate[] = [
|
||||||
{
|
{
|
||||||
name: "Due date before",
|
name: "before",
|
||||||
value: "before",
|
value: "before",
|
||||||
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
|
icon: <CalendarBeforeIcon className="h-4 w-4 " />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Due date after",
|
name: "after",
|
||||||
value: "after",
|
value: "after",
|
||||||
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
icon: <CalendarAfterIcon className="h-4 w-4 " />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Due date range",
|
name: "range",
|
||||||
value: "range",
|
value: "range",
|
||||||
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
icon: <CalendarMonthIcon className="h-4 w-4 " />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
export const DateFilterSelect: React.FC<Props> = ({ title, value, onChange }) => (
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={value}
|
value={value}
|
||||||
label={
|
label={
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
{dueDateRange.find((item) => item.value === value)?.icon}
|
{dueDateRange.find((item) => item.value === value)?.icon}
|
||||||
<span>{dueDateRange.find((item) => item.value === value)?.name}</span>
|
<span>
|
||||||
|
{title} {dueDateRange.find((item) => item.value === value)?.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -50,7 +53,7 @@ export const DueDateFilterSelect: React.FC<Props> = ({ value, onChange }) => (
|
|||||||
<CustomSelect.Option key={index} value={option.value}>
|
<CustomSelect.Option key={index} value={option.value}>
|
||||||
<>
|
<>
|
||||||
<span>{option.icon}</span>
|
<span>{option.icon}</span>
|
||||||
{option.name}
|
{title} {option.name}
|
||||||
</>
|
</>
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
))}
|
))}
|
@ -240,6 +240,34 @@ export const FiltersList: React.FC<Props> = ({
|
|||||||
</div>
|
</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"
|
: key === "target_date"
|
||||||
? filters.target_date?.map((date: string) => {
|
? filters.target_date?.map((date: string) => {
|
||||||
if (filters.target_date && filters.target_date.length <= 0) return null;
|
if (filters.target_date && filters.target_date.length <= 0) return null;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./due-date-filter-modal";
|
export * from "./date-filter-modal";
|
||||||
export * from "./due-date-filter-select";
|
export * from "./date-filter-select";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
export * from "./issues-view-filter";
|
export * from "./issues-view-filter";
|
||||||
|
@ -119,14 +119,11 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (key === "target_date") {
|
if (key === "start_date" || key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(filters[key] ?? [], option.value);
|
||||||
filters.target_date ?? [],
|
|
||||||
option.value
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters({
|
setFilters({
|
||||||
target_date: valueExists ? null : option.value,
|
[key]: valueExists ? null : option.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
|
@ -12,7 +12,7 @@ import useProjects from "hooks/use-projects";
|
|||||||
// component
|
// component
|
||||||
import { Avatar, Icon } from "components/ui";
|
import { Avatar, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import { PlusIcon } from "@heroicons/react/24/outline";
|
||||||
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
@ -56,10 +56,10 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: members } = useSWR(
|
const { data: members } = useSWR(
|
||||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||||
? PROJECT_MEMBERS(projectId.toString())
|
? PROJECT_MEMBERS(projectId.toString())
|
||||||
: null,
|
: null,
|
||||||
workspaceSlug && projectId && selectedGroup === "created_by"
|
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
|
||||||
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
@ -79,9 +79,11 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
|
case "assignees":
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
title = member?.display_name ?? "";
|
title = member ? member.display_name : "None";
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +124,10 @@ export const BoardHeader: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "assignees":
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
|
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -478,6 +478,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state: null,
|
state: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
type: null,
|
||||||
})
|
})
|
||||||
@ -513,7 +514,8 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
dragDisabled={
|
dragDisabled={
|
||||||
selectedGroup === "created_by" ||
|
selectedGroup === "created_by" ||
|
||||||
selectedGroup === "labels" ||
|
selectedGroup === "labels" ||
|
||||||
selectedGroup === "state_detail.group"
|
selectedGroup === "state_detail.group" ||
|
||||||
|
selectedGroup === "assignees"
|
||||||
}
|
}
|
||||||
emptyState={{
|
emptyState={{
|
||||||
title: cycleId
|
title: cycleId
|
||||||
@ -546,7 +548,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
}}
|
}}
|
||||||
handleOnDragEnd={handleOnDragEnd}
|
handleOnDragEnd={handleOnDragEnd}
|
||||||
handleIssueAction={handleIssueAction}
|
handleIssueAction={handleIssueAction}
|
||||||
openIssuesListModal={openIssuesListModal ? openIssuesListModal : null}
|
openIssuesListModal={openIssuesListModal ?? null}
|
||||||
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
removeIssue={cycleId ? removeIssueFromCycle : moduleId ? removeIssueFromModule : null}
|
||||||
trashBox={trashBox}
|
trashBox={trashBox}
|
||||||
setTrashBox={setTrashBox}
|
setTrashBox={setTrashBox}
|
||||||
|
@ -94,9 +94,10 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
case "project":
|
case "project":
|
||||||
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
title = projects?.find((p) => p.id === groupTitle)?.name ?? "None";
|
||||||
break;
|
break;
|
||||||
|
case "assignees":
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
title = member?.display_name ?? "";
|
title = member ? member.display_name : "None";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,9 +138,10 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "assignees":
|
||||||
case "created_by":
|
case "created_by":
|
||||||
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
const member = members?.find((member) => member.member.id === groupTitle)?.member;
|
||||||
icon = <Avatar user={member} height="24px" width="24px" fontSize="12px" />;
|
icon = member ? <Avatar user={member} height="24px" width="24px" fontSize="12px" /> : <></>;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
|
IssuePeekOverview,
|
||||||
ViewAssigneeSelect,
|
ViewAssigneeSelect,
|
||||||
ViewDueDateSelect,
|
ViewDueDateSelect,
|
||||||
ViewEstimateSelect,
|
ViewEstimateSelect,
|
||||||
@ -75,6 +75,10 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
nestingLevel,
|
nestingLevel,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
// issue peek overview
|
||||||
|
const [issuePeekOverview, setIssuePeekOverview] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
@ -95,7 +99,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
? VIEW_ISSUES(viewId.toString(), params)
|
? VIEW_ISSUES(viewId.toString(), params)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
|
||||||
|
|
||||||
if (issue.parent) {
|
if (issue.parent)
|
||||||
mutate<ISubIssueResponse>(
|
mutate<ISubIssueResponse>(
|
||||||
SUB_ISSUES(issue.parent.toString()),
|
SUB_ISSUES(issue.parent.toString()),
|
||||||
(prevData) => {
|
(prevData) => {
|
||||||
@ -116,7 +120,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
else
|
||||||
mutate<IIssue[]>(
|
mutate<IIssue[]>(
|
||||||
fetchKey,
|
fetchKey,
|
||||||
(prevData) =>
|
(prevData) =>
|
||||||
@ -131,7 +135,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
}),
|
}),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
issuesService
|
issuesService
|
||||||
.patchIssue(
|
.patchIssue(
|
||||||
@ -179,190 +182,203 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
|||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
<IssuePeekOverview
|
||||||
style={{ gridTemplateColumns }}
|
handleDeleteIssue={() => handleDeleteIssue(issue)}
|
||||||
>
|
handleUpdateIssue={async (formData) => partialUpdateIssue(formData, issue)}
|
||||||
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
|
issue={issue}
|
||||||
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
|
isOpen={issuePeekOverview}
|
||||||
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
|
onClose={() => setIssuePeekOverview(false)}
|
||||||
{properties.key && (
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
|
readOnly={isNotAllowed}
|
||||||
{issue.project_detail?.identifier}-{issue.sequence_id}
|
/>
|
||||||
</span>
|
<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"
|
||||||
{!isNotAllowed && !disableUserActions && (
|
style={{ gridTemplateColumns }}
|
||||||
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
>
|
||||||
<Popover2
|
<div className="flex gap-1.5 items-center px-4 sticky z-[1] left-0 text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full">
|
||||||
isOpen={isOpen}
|
<div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
|
||||||
canEscapeKeyClose
|
<div className="relative flex items-center cursor-pointer text-xs text-center hover:text-custom-text-100 w-14">
|
||||||
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
{properties.key && (
|
||||||
content={
|
<span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
|
||||||
<div
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
</span>
|
||||||
>
|
)}
|
||||||
<button
|
{!isNotAllowed && !disableUserActions && (
|
||||||
type="button"
|
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
|
||||||
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"
|
<Popover2
|
||||||
onClick={() => {
|
isOpen={isOpen}
|
||||||
handleEditIssue(issue);
|
canEscapeKeyClose
|
||||||
setIsOpen(false);
|
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
|
||||||
}}
|
content={
|
||||||
|
<div
|
||||||
|
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-custom-border-200 bg-custom-background-90`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<button
|
||||||
<PencilIcon className="h-4 w-4" />
|
type="button"
|
||||||
<span>Edit issue</span>
|
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"
|
||||||
</div>
|
onClick={() => {
|
||||||
</button>
|
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
|
<button
|
||||||
type="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"
|
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={() => {
|
onClick={() => {
|
||||||
handleDeleteIssue(issue);
|
handleDeleteIssue(issue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<TrashIcon className="h-4 w-4" />
|
<TrashIcon className="h-4 w-4" />
|
||||||
<span>Delete issue</span>
|
<span>Delete issue</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="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"
|
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={() => {
|
onClick={() => {
|
||||||
handleCopyText();
|
handleCopyText();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-4 w-4" />
|
<LinkIcon className="h-4 w-4" />
|
||||||
<span>Copy issue link</span>
|
<span>Copy issue link</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="bottom-start"
|
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)}
|
||||||
>
|
>
|
||||||
<EllipsisHorizontalIcon className="h-5 w-5 text-custom-text-200" />
|
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
||||||
</Popover2>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{issue.sub_issues_count > 0 && (
|
<button
|
||||||
<div className="h-6 w-6 flex justify-center items-center">
|
type="button"
|
||||||
<button
|
className="truncate text-custom-text-100 text-left cursor-pointer w-full text-[0.825rem]"
|
||||||
className="h-5 w-5 hover:bg-custom-background-90 hover:text-custom-text-100 rounded-sm cursor-pointer"
|
onClick={() => setIssuePeekOverview(true)}
|
||||||
onClick={() => handleToggleExpand(issue.id)}
|
>
|
||||||
>
|
|
||||||
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`}>
|
|
||||||
<a className="truncate text-custom-text-100 cursor-pointer w-full text-[0.825rem]">
|
|
||||||
{issue.name}
|
{issue.name}
|
||||||
</a>
|
</button>
|
||||||
</Link>
|
</div>
|
||||||
|
{properties.state && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewStateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
className="max-w-full"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
customButton
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.priority && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewPrioritySelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.assignee && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewAssigneeSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
customButton
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.labels && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewIssueLabel issue={issue} maxRender={1} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{properties.start_date && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewStartDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{properties.due_date && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewDueDateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
noBorder
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.estimate && (
|
||||||
|
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
<ViewEstimateSelect
|
||||||
|
issue={issue}
|
||||||
|
partialUpdateIssue={partialUpdateIssue}
|
||||||
|
position="left"
|
||||||
|
tooltipPosition={tooltipPosition}
|
||||||
|
user={user}
|
||||||
|
isNotAllowed={isNotAllowed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.created_on && (
|
||||||
|
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
{renderLongDetailDateFormat(issue.created_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{properties.updated_on && (
|
||||||
|
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||||
|
{renderLongDetailDateFormat(issue.updated_at)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{properties.state && (
|
</>
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewStateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
position="left"
|
|
||||||
className="max-w-full"
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
customButton
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.priority && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewPrioritySelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
position="left"
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
noBorder
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.assignee && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewAssigneeSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
position="left"
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
customButton
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.labels && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewIssueLabel issue={issue} maxRender={1} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.start_date && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewStartDateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
noBorder
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{properties.due_date && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewDueDateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
noBorder
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.estimate && (
|
|
||||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
<ViewEstimateSelect
|
|
||||||
issue={issue}
|
|
||||||
partialUpdateIssue={partialUpdateIssue}
|
|
||||||
position="left"
|
|
||||||
tooltipPosition={tooltipPosition}
|
|
||||||
user={user}
|
|
||||||
isNotAllowed={isNotAllowed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.created_on && (
|
|
||||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
{renderLongDetailDateFormat(issue.created_at)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{properties.updated_on && (
|
|
||||||
<div className="flex items-center text-xs cursor-default text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
|
||||||
{renderLongDetailDateFormat(issue.updated_at)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
||||||
import { CustomMenu, Icon, Spinner } from "components/ui";
|
import { CustomMenu, Spinner } from "components/ui";
|
||||||
// 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 useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
106
apps/app/components/inbox/inbox-issue-activity.tsx
Normal file
106
apps/app/components/inbox/inbox-issue-activity.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { AddComment, IssueActivitySection } from "components/issues";
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueComment } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = { issueDetails: IIssue };
|
||||||
|
|
||||||
|
export const InboxIssueActivity: React.FC<Props> = ({ issueDetails }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||||
|
workspaceSlug && projectId && inboxIssueId
|
||||||
|
? PROJECT_ISSUES_ACTIVITY(inboxIssueId.toString())
|
||||||
|
: null,
|
||||||
|
workspaceSlug && projectId && inboxIssueId
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssueActivities(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
inboxIssueId.toString()
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !projectId || !inboxIssueId) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.patchIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
inboxIssueId as string,
|
||||||
|
comment.id,
|
||||||
|
comment,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommentDelete = async (commentId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId || !inboxIssueId) return;
|
||||||
|
|
||||||
|
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.deleteIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
inboxIssueId as string,
|
||||||
|
commentId,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !issueDetails) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssueComment(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
issueDetails.project,
|
||||||
|
issueDetails.id,
|
||||||
|
formData,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Comment could not be posted. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||||
|
<IssueActivitySection
|
||||||
|
activity={issueActivity}
|
||||||
|
handleCommentUpdate={handleCommentUpdate}
|
||||||
|
handleCommentDelete={handleCommentDelete}
|
||||||
|
/>
|
||||||
|
<AddComment onSubmit={handleAddComment} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -14,13 +14,8 @@ import inboxServices from "services/inbox.service";
|
|||||||
import useInboxView from "hooks/use-inbox-view";
|
import useInboxView from "hooks/use-inbox-view";
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
// components
|
// components
|
||||||
import {
|
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues";
|
||||||
AddComment,
|
import { InboxIssueActivity } from "components/inbox";
|
||||||
IssueActivitySection,
|
|
||||||
IssueDescriptionForm,
|
|
||||||
IssueDetailsSidebar,
|
|
||||||
IssueReaction,
|
|
||||||
} from "components/issues";
|
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -42,7 +37,6 @@ import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "cons
|
|||||||
|
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
|
||||||
description_html: "",
|
description_html: "",
|
||||||
estimate_point: null,
|
estimate_point: null,
|
||||||
assignees_list: [],
|
assignees_list: [],
|
||||||
@ -296,7 +290,6 @@ export const InboxMainContent: React.FC = () => {
|
|||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={{
|
issue={{
|
||||||
name: issueDetails.name,
|
name: issueDetails.name,
|
||||||
description: issueDetails.description,
|
|
||||||
description_html: issueDetails.description_html,
|
description_html: issueDetails.description_html,
|
||||||
}}
|
}}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
@ -312,11 +305,7 @@ export const InboxMainContent: React.FC = () => {
|
|||||||
issueId={issueDetails.id}
|
issueId={issueDetails.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-5">
|
<InboxIssueActivity issueDetails={issueDetails} />
|
||||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
|
||||||
<IssueActivitySection issueId={issueDetails.id} user={user} />
|
|
||||||
<AddComment issueId={issueDetails.id} user={user} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="basis-1/3 space-y-5 border-custom-border-200 p-5">
|
<div className="basis-1/3 space-y-5 border-custom-border-200 p-5">
|
||||||
|
@ -4,6 +4,7 @@ export * from "./delete-issue-modal";
|
|||||||
export * from "./filters-dropdown";
|
export * from "./filters-dropdown";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
export * from "./inbox-action-headers";
|
export * from "./inbox-action-headers";
|
||||||
|
export * from "./inbox-issue-activity";
|
||||||
export * from "./inbox-issue-card";
|
export * from "./inbox-issue-card";
|
||||||
export * from "./inbox-main-content";
|
export * from "./inbox-main-content";
|
||||||
export * from "./issues-list-sidebar";
|
export * from "./issues-list-sidebar";
|
||||||
|
@ -3,10 +3,6 @@ import React from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// services
|
|
||||||
import issuesService from "services/issues.service";
|
|
||||||
// components
|
// components
|
||||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||||
import { CommentCard } from "components/issues/comment";
|
import { CommentCard } from "components/issues/comment";
|
||||||
@ -15,62 +11,23 @@ import { Icon, Loader } from "components/ui";
|
|||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IIssueComment } from "types";
|
import { IIssueActivity, IIssueComment } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
activity: IIssueActivity[] | undefined;
|
||||||
user: ICurrentUserResponse | undefined;
|
handleCommentUpdate: (comment: IIssueComment) => Promise<void>;
|
||||||
|
handleCommentDelete: (commentId: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
export const IssueActivitySection: React.FC<Props> = ({
|
||||||
|
activity,
|
||||||
|
handleCommentUpdate,
|
||||||
|
handleCommentDelete,
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { data: issueActivities, mutate: mutateIssueActivities } = useSWR(
|
if (!activity)
|
||||||
workspaceSlug && projectId ? PROJECT_ISSUES_ACTIVITY(issueId) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () =>
|
|
||||||
issuesService.getIssueActivities(workspaceSlug as string, projectId as string, issueId)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCommentUpdate = async (comment: IIssueComment) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
await issuesService
|
|
||||||
.patchIssueComment(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
issueId as string,
|
|
||||||
comment.id,
|
|
||||||
comment,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.then((res) => mutateIssueActivities());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommentDelete = async (commentId: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutateIssueActivities(
|
|
||||||
(prevData: any) => prevData?.filter((p: any) => p.id !== commentId),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
await issuesService
|
|
||||||
.deleteIssueComment(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
issueId as string,
|
|
||||||
commentId,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.then(() => mutateIssueActivities());
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!issueActivities) {
|
|
||||||
return (
|
return (
|
||||||
<Loader className="space-y-4">
|
<Loader className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -87,12 +44,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flow-root">
|
<div className="flow-root">
|
||||||
<ul role="list" className="-mb-4">
|
<ul role="list" className="-mb-4">
|
||||||
{issueActivities.map((activityItem, index) => {
|
{activity.map((activityItem, index) => {
|
||||||
// determines what type of action is performed
|
// determines what type of action is performed
|
||||||
const message = activityItem.field ? (
|
const message = activityItem.field ? (
|
||||||
<ActivityMessage activity={activityItem} />
|
<ActivityMessage activity={activityItem} />
|
||||||
@ -104,7 +60,7 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
|||||||
return (
|
return (
|
||||||
<li key={activityItem.id}>
|
<li key={activityItem.id}>
|
||||||
<div className="relative pb-1">
|
<div className="relative pb-1">
|
||||||
{issueActivities.length > 1 && index !== issueActivities.length - 1 ? (
|
{activity.length > 1 && index !== activity.length - 1 ? (
|
||||||
<span
|
<span
|
||||||
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
className="absolute top-5 left-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
@ -2,20 +2,13 @@ import React from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { mutate } from "swr";
|
|
||||||
|
|
||||||
// react-hook-form
|
// react-hook-form
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
// services
|
|
||||||
import issuesServices from "services/issues.service";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
// ui
|
||||||
import { SecondaryButton } from "components/ui";
|
import { SecondaryButton } from "components/ui";
|
||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IIssueComment } from "types";
|
import type { IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
|
||||||
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
import Tiptap, { ITiptapRichTextEditor } from "components/tiptap";
|
||||||
|
|
||||||
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
const TiptapEditor = React.forwardRef<ITiptapRichTextEditor, ITiptapRichTextEditor>(
|
||||||
@ -30,63 +23,37 @@ const defaultValues: Partial<IIssueComment> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
onSubmit: (data: IIssueComment) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false }) => {
|
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit }) => {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
|
||||||
control,
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting },
|
|
||||||
reset,
|
|
||||||
} = useForm<IIssueComment>({ defaultValues });
|
} = useForm<IIssueComment>({ defaultValues });
|
||||||
|
|
||||||
const editorRef = React.useRef<any>(null);
|
const editorRef = React.useRef<any>(null);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!formData.comment_html || !formData.comment_json || isSubmitting) return;
|
||||||
|
|
||||||
const onSubmit = async (formData: IIssueComment) => {
|
await onSubmit(formData).then(() => {
|
||||||
if (
|
reset(defaultValues);
|
||||||
!workspaceSlug ||
|
editorRef.current?.clearEditor();
|
||||||
!projectId ||
|
});
|
||||||
!issueId ||
|
|
||||||
isSubmitting ||
|
|
||||||
!formData.comment_html ||
|
|
||||||
!formData.comment_json
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
await issuesServices
|
|
||||||
.createIssueComment(
|
|
||||||
workspaceSlug as string,
|
|
||||||
projectId as string,
|
|
||||||
issueId as string,
|
|
||||||
formData,
|
|
||||||
user
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueId as string));
|
|
||||||
reset(defaultValues);
|
|
||||||
editorRef.current?.clearEditor();
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Comment could not be posted. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||||
<div className="issue-comments-section">
|
<div className="issue-comments-section">
|
||||||
<Controller
|
<Controller
|
||||||
name="comment_html"
|
name="comment_html"
|
||||||
@ -97,8 +64,8 @@ export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false })
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
value={
|
value={
|
||||||
!value ||
|
!value ||
|
||||||
value === "" ||
|
value === "" ||
|
||||||
(typeof value === "object" && Object.keys(value).length === 0)
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
? watch("comment_html")
|
? watch("comment_html")
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
|
@ -4,24 +4,21 @@ import { FC, useCallback, useEffect, useState } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
// hooks
|
// hooks
|
||||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
// components
|
// components
|
||||||
import { TextArea } from "components/ui";
|
import { TextArea } from "components/ui";
|
||||||
|
import Tiptap from "components/tiptap";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
import Tiptap from "components/tiptap";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
|
||||||
|
|
||||||
export interface IssueDescriptionFormValues {
|
export interface IssueDescriptionFormValues {
|
||||||
name: string;
|
name: string;
|
||||||
description: any;
|
|
||||||
description_html: string;
|
description_html: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IssueDetailsProps {
|
export interface IssueDetailsProps {
|
||||||
issue: {
|
issue: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
|
||||||
description_html: string;
|
description_html: string;
|
||||||
};
|
};
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
@ -43,7 +40,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch,
|
watch,
|
||||||
setValue,
|
|
||||||
reset,
|
reset,
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
@ -51,7 +47,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
} = useForm<IIssue>({
|
} = useForm<IIssue>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
|
||||||
description_html: "",
|
description_html: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -62,7 +57,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
|
|
||||||
await handleFormSubmit({
|
await handleFormSubmit({
|
||||||
name: formData.name ?? "",
|
name: formData.name ?? "",
|
||||||
description: formData.description ?? "",
|
|
||||||
description_html: formData.description_html ?? "<p></p>",
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -80,7 +74,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isSubmitting, setShowAlert]);
|
}, [isSubmitting, setShowAlert]);
|
||||||
|
|
||||||
|
|
||||||
// reset form values
|
// reset form values
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
@ -99,27 +92,32 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<TextArea
|
{isAllowed ? (
|
||||||
id="name"
|
<TextArea
|
||||||
name="name"
|
id="name"
|
||||||
placeholder="Enter issue name"
|
name="name"
|
||||||
register={register}
|
placeholder="Enter issue name"
|
||||||
onFocus={() => setCharacterLimit(true)}
|
register={register}
|
||||||
onChange={(e) => {
|
onFocus={() => setCharacterLimit(true)}
|
||||||
setCharacterLimit(false);
|
onChange={(e) => {
|
||||||
setIsSubmitting("submitting");
|
setCharacterLimit(false);
|
||||||
debouncedTitleSave();
|
setIsSubmitting("submitting");
|
||||||
}}
|
debouncedTitleSave();
|
||||||
required={true}
|
}}
|
||||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
required={true}
|
||||||
role="textbox"
|
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
|
||||||
disabled={!isAllowed}
|
role="textbox"
|
||||||
/>
|
disabled={!isAllowed}
|
||||||
{characterLimit && (
|
/>
|
||||||
|
) : (
|
||||||
|
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
||||||
|
)}
|
||||||
|
{characterLimit && isAllowed && (
|
||||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
||||||
<span
|
<span
|
||||||
className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
className={`${
|
||||||
}`}
|
watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{watch("name").length}
|
{watch("name").length}
|
||||||
</span>
|
</span>
|
||||||
@ -133,39 +131,42 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
name="description_html"
|
name="description_html"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { value, onChange } }) => {
|
||||||
if (!value && !watch("description_html")) return <></>;
|
if (!value) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tiptap
|
<Tiptap
|
||||||
value={
|
value={
|
||||||
!value ||
|
!value ||
|
||||||
value === "" ||
|
value === "" ||
|
||||||
(typeof value === "object" && Object.keys(value).length === 0)
|
(typeof value === "object" && Object.keys(value).length === 0)
|
||||||
? watch("description_html")
|
? "<p></p>"
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
debouncedUpdatesEnabled={true}
|
debouncedUpdatesEnabled={true}
|
||||||
setShouldShowAlert={setShowAlert}
|
setShouldShowAlert={setShowAlert}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
customClassName="min-h-[150px] shadow-sm"
|
customClassName={
|
||||||
editorContentCustomClassNames="pb-9"
|
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"
|
||||||
|
}
|
||||||
|
noBorder={!isAllowed}
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
setShowAlert(true);
|
setShowAlert(true);
|
||||||
setIsSubmitting("submitting");
|
setIsSubmitting("submitting");
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
setValue("description", description);
|
handleSubmit(handleDescriptionFormSubmit)().finally(() =>
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => {
|
setIsSubmitting("submitted")
|
||||||
setIsSubmitting("submitted");
|
);
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
|
editable={isAllowed}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||||
}`}
|
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,3 +15,4 @@ export * from "./sidebar";
|
|||||||
export * from "./sub-issues-list";
|
export * from "./sub-issues-list";
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
|
export * from "./peek-overview";
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import useUserAuth from "hooks/use-user-auth";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
// contexts
|
// contexts
|
||||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||||
// components
|
// components
|
||||||
@ -25,9 +26,9 @@ import { CustomMenu } from "components/ui";
|
|||||||
import { LayerDiagonalIcon } from "components/icons";
|
import { LayerDiagonalIcon } from "components/icons";
|
||||||
import { MinusCircleIcon } from "@heroicons/react/24/outline";
|
import { MinusCircleIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue, IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issueDetails: IIssue;
|
issueDetails: IIssue;
|
||||||
@ -43,6 +44,8 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
|
|
||||||
@ -50,15 +53,81 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
||||||
workspaceSlug && projectId && issueDetails?.parent
|
workspaceSlug && projectId && issueDetails?.parent
|
||||||
? () =>
|
? () =>
|
||||||
issuesService.subIssues(
|
issuesService.subIssues(
|
||||||
workspaceSlug as string,
|
workspaceSlug as string,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
issueDetails.parent ?? ""
|
issueDetails.parent ?? ""
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
|
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
|
||||||
|
|
||||||
|
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||||
|
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
|
||||||
|
workspaceSlug && projectId && issueId
|
||||||
|
? () =>
|
||||||
|
issuesService.getIssueActivities(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issueId.toString()
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.patchIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
comment.id,
|
||||||
|
comment,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommentDelete = async (commentId: string) => {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) return;
|
||||||
|
|
||||||
|
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.deleteIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId as string,
|
||||||
|
issueId as string,
|
||||||
|
commentId,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !issueDetails) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssueComment(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
issueDetails.project,
|
||||||
|
issueDetails.id,
|
||||||
|
formData,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Comment could not be posted. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-lg">
|
<div className="rounded-lg">
|
||||||
@ -97,8 +166,9 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
key={issue.id}
|
key={issue.id}
|
||||||
renderAs="a"
|
renderAs="a"
|
||||||
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id
|
href={`/${workspaceSlug}/projects/${projectId as string}/issues/${
|
||||||
}`}
|
issue.id
|
||||||
|
}`}
|
||||||
className="flex items-center gap-2 py-2"
|
className="flex items-center gap-2 py-2"
|
||||||
>
|
>
|
||||||
<LayerDiagonalIcon className="h-4 w-4" />
|
<LayerDiagonalIcon className="h-4 w-4" />
|
||||||
@ -146,14 +216,11 @@ export const IssueMainContent: React.FC<Props> = ({
|
|||||||
<div className="space-y-5 pt-3">
|
<div className="space-y-5 pt-3">
|
||||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||||
<IssueActivitySection
|
<IssueActivitySection
|
||||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
activity={issueActivity}
|
||||||
user={user}
|
handleCommentUpdate={handleCommentUpdate}
|
||||||
/>
|
handleCommentDelete={handleCommentDelete}
|
||||||
<AddComment
|
|
||||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
|
||||||
user={user}
|
|
||||||
disabled={uneditable}
|
|
||||||
/>
|
/>
|
||||||
|
<AddComment onSubmit={handleAddComment} disabled={uneditable} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import useSWR from "swr";
|
|||||||
// services
|
// services
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// components
|
// components
|
||||||
import { DueDateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { MultiLevelDropdown } from "components/ui";
|
import { MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -20,7 +20,7 @@ import { IIssueFilterOptions, IQuery } from "types";
|
|||||||
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
import { WORKSPACE_LABELS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
import { GROUP_CHOICES, PRIORITIES } from "constants/project";
|
||||||
import { DUE_DATES } from "constants/due-dates";
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||||
@ -35,7 +35,14 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
direction = "right",
|
direction = "right",
|
||||||
height = "md",
|
height = "md",
|
||||||
}) => {
|
}) => {
|
||||||
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
|
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||||
|
const [dateFilterType, setDateFilterType] = useState<{
|
||||||
|
title: string;
|
||||||
|
type: "start_date" | "target_date";
|
||||||
|
}>({
|
||||||
|
title: "",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
const [fetchLabels, setFetchLabels] = useState(false);
|
const [fetchLabels, setFetchLabels] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -50,10 +57,12 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDueDateFilterModalOpen && (
|
{isDateFilterModalOpen && (
|
||||||
<DueDateFilterModal
|
<DateFilterModal
|
||||||
isOpen={isDueDateFilterModalOpen}
|
title={dateFilterType.title}
|
||||||
handleClose={() => setIsDueDateFilterModalOpen(false)}
|
field={dateFilterType.type}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MultiLevelDropdown
|
<MultiLevelDropdown
|
||||||
@ -132,12 +141,48 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "target_date",
|
id: "start_date",
|
||||||
label: "Due date",
|
label: "Start date",
|
||||||
value: DUE_DATES,
|
value: DATE_FILTER_OPTIONS,
|
||||||
hasChildren: true,
|
hasChildren: true,
|
||||||
children: [
|
children: [
|
||||||
...(DUE_DATES?.map((option) => ({
|
...(DATE_FILTER_OPTIONS?.map((option) => ({
|
||||||
|
id: option.name,
|
||||||
|
label: option.name,
|
||||||
|
value: {
|
||||||
|
key: "start_date",
|
||||||
|
value: option.value,
|
||||||
|
},
|
||||||
|
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value),
|
||||||
|
})) ?? []),
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
label: "Custom",
|
||||||
|
value: "custom",
|
||||||
|
element: (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Start date",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "target_date",
|
||||||
|
label: "Due date",
|
||||||
|
value: DATE_FILTER_OPTIONS,
|
||||||
|
hasChildren: true,
|
||||||
|
children: [
|
||||||
|
...(DATE_FILTER_OPTIONS?.map((option) => ({
|
||||||
id: option.name,
|
id: option.name,
|
||||||
label: option.name,
|
label: option.name,
|
||||||
value: {
|
value: {
|
||||||
@ -152,7 +197,13 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
|
|||||||
value: "custom",
|
value: "custom",
|
||||||
element: (
|
element: (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDueDateFilterModalOpen(true)}
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Due date",
|
||||||
|
type: "target_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
>
|
>
|
||||||
Custom
|
Custom
|
||||||
|
@ -89,14 +89,11 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (key === "target_date") {
|
if (key === "start_date" || key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||||
filters?.target_date ?? [],
|
|
||||||
option.value
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters({
|
setFilters({
|
||||||
target_date: valueExists ? null : option.value,
|
[key]: valueExists ? null : option.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
@ -159,7 +156,11 @@ export const MyIssuesViewOptions: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
if (issueView === "kanban" && option.key === null) return null;
|
if (issueView === "kanban" && option.key === null) return null;
|
||||||
if (option.key === "state" || option.key === "created_by")
|
if (
|
||||||
|
option.key === "state" ||
|
||||||
|
option.key === "created_by" ||
|
||||||
|
option.key === "assignees"
|
||||||
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -249,6 +249,7 @@ export const MyIssuesView: React.FC<Props> = ({
|
|||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state_group: null,
|
state_group: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
type: null,
|
||||||
})
|
})
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
PeekOverviewHeader,
|
||||||
|
PeekOverviewIssueActivity,
|
||||||
|
PeekOverviewIssueDetails,
|
||||||
|
PeekOverviewIssueProperties,
|
||||||
|
TPeekOverviewModes,
|
||||||
|
} from "components/issues";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
handleDeleteIssue: () => void;
|
||||||
|
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||||
|
issue: IIssue;
|
||||||
|
mode: TPeekOverviewModes;
|
||||||
|
readOnly: boolean;
|
||||||
|
setMode: (mode: TPeekOverviewModes) => void;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FullScreenPeekView: React.FC<Props> = ({
|
||||||
|
handleClose,
|
||||||
|
handleDeleteIssue,
|
||||||
|
handleUpdateIssue,
|
||||||
|
issue,
|
||||||
|
mode,
|
||||||
|
readOnly,
|
||||||
|
setMode,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => (
|
||||||
|
<div className="h-full w-full grid grid-cols-10 divide-x divide-custom-border-200 overflow-hidden">
|
||||||
|
<div className="h-full w-full flex flex-col col-span-7 overflow-hidden">
|
||||||
|
<div className="w-full p-5">
|
||||||
|
<PeekOverviewHeader
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode={mode}
|
||||||
|
setMode={setMode}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-full px-6 overflow-y-auto">
|
||||||
|
{/* issue title and description */}
|
||||||
|
<div className="w-full">
|
||||||
|
<PeekOverviewIssueDetails
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issue={issue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* divider */}
|
||||||
|
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||||
|
{/* issue activity/comments */}
|
||||||
|
<div className="w-full">
|
||||||
|
<PeekOverviewIssueActivity
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
issue={issue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 h-full w-full overflow-y-auto">
|
||||||
|
{/* issue properties */}
|
||||||
|
<div className="w-full px-6 py-5">
|
||||||
|
<PeekOverviewIssueProperties
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode="full"
|
||||||
|
onChange={handleUpdateIssue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
133
apps/app/components/issues/peek-overview/header.tsx
Normal file
133
apps/app/components/issues/peek-overview/header.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { CustomSelect, Icon } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
import { TPeekOverviewModes } from "./layout";
|
||||||
|
import { ArrowRightAlt, CloseFullscreen, East, OpenInFull } from "@mui/icons-material";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
handleDeleteIssue: () => void;
|
||||||
|
issue: IIssue;
|
||||||
|
mode: TPeekOverviewModes;
|
||||||
|
setMode: (mode: TPeekOverviewModes) => void;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const peekModes: {
|
||||||
|
key: TPeekOverviewModes;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
}[] = [
|
||||||
|
{ key: "side", icon: "side_navigation", label: "Side Peek" },
|
||||||
|
{
|
||||||
|
key: "modal",
|
||||||
|
icon: "dialogs",
|
||||||
|
label: "Modal Peek",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "full",
|
||||||
|
icon: "nearby",
|
||||||
|
label: "Full Screen Peek",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PeekOverviewHeader: React.FC<Props> = ({
|
||||||
|
issue,
|
||||||
|
handleClose,
|
||||||
|
handleDeleteIssue,
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => {
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const handleCopyLink = () => {
|
||||||
|
const originURL =
|
||||||
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
|
||||||
|
copyTextToClipboard(
|
||||||
|
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link copied!",
|
||||||
|
message: "Issue link copied to clipboard",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{mode === "side" && (
|
||||||
|
<button type="button" onClick={handleClose}>
|
||||||
|
<East
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{mode === "modal" || mode === "full" ? (
|
||||||
|
<button type="button" onClick={() => setMode("side")}>
|
||||||
|
<CloseFullscreen
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={() => setMode("modal")}>
|
||||||
|
<OpenInFull
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<CustomSelect
|
||||||
|
value={mode}
|
||||||
|
onChange={(val: TPeekOverviewModes) => setMode(val)}
|
||||||
|
customButton={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`grid place-items-center ${mode === "full" ? "rotate-45" : ""}`}
|
||||||
|
>
|
||||||
|
<Icon iconName={peekModes.find((m) => m.key === mode)?.icon ?? ""} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
{peekModes.map((mode) => (
|
||||||
|
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Icon
|
||||||
|
iconName={mode.icon}
|
||||||
|
className={`!text-base flex-shrink-0 -my-1 ${
|
||||||
|
mode.key === "full" ? "rotate-45" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{mode.label}
|
||||||
|
</div>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))}
|
||||||
|
</CustomSelect>
|
||||||
|
</div>
|
||||||
|
{(mode === "side" || mode === "modal") && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||||
|
<Icon iconName="link" />
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleDeleteIssue}>
|
||||||
|
<Icon iconName="delete" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
7
apps/app/components/issues/peek-overview/index.ts
Normal file
7
apps/app/components/issues/peek-overview/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export * from "./full-screen-peek-view";
|
||||||
|
export * from "./header";
|
||||||
|
export * from "./issue-activity";
|
||||||
|
export * from "./issue-details";
|
||||||
|
export * from "./issue-properties";
|
||||||
|
export * from "./layout";
|
||||||
|
export * from "./side-peek-view";
|
90
apps/app/components/issues/peek-overview/issue-activity.tsx
Normal file
90
apps/app/components/issues/peek-overview/issue-activity.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
// hooks
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// components
|
||||||
|
import { AddComment, IssueActivitySection } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue, IIssueComment } from "types";
|
||||||
|
// fetch-keys
|
||||||
|
import { PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
issue: IIssue;
|
||||||
|
readOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PeekOverviewIssueActivity: React.FC<Props> = ({ workspaceSlug, issue, readOnly }) => {
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
||||||
|
workspaceSlug && issue ? PROJECT_ISSUES_ACTIVITY(issue.id) : null,
|
||||||
|
workspaceSlug && issue
|
||||||
|
? () => issuesService.getIssueActivities(workspaceSlug.toString(), issue?.project, issue?.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCommentUpdate = async (comment: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !issue) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.patchIssueComment(
|
||||||
|
workspaceSlug as string,
|
||||||
|
issue.project,
|
||||||
|
issue.id,
|
||||||
|
comment.id,
|
||||||
|
comment,
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommentDelete = async (commentId: string) => {
|
||||||
|
if (!workspaceSlug || !issue) return;
|
||||||
|
|
||||||
|
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.deleteIssueComment(workspaceSlug as string, issue.project, issue.id, commentId, user)
|
||||||
|
.then(() => mutateIssueActivity());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddComment = async (formData: IIssueComment) => {
|
||||||
|
if (!workspaceSlug || !issue) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssueComment(workspaceSlug.toString(), issue.project, issue.id, formData, user)
|
||||||
|
.then(() => {
|
||||||
|
mutate(PROJECT_ISSUES_ACTIVITY(issue.id));
|
||||||
|
})
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Comment could not be posted. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Activity</h4>
|
||||||
|
<div className="mt-4">
|
||||||
|
<IssueActivitySection
|
||||||
|
activity={issueActivity}
|
||||||
|
handleCommentUpdate={handleCommentUpdate}
|
||||||
|
handleCommentDelete={handleCommentDelete}
|
||||||
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<AddComment onSubmit={handleAddComment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
34
apps/app/components/issues/peek-overview/issue-details.tsx
Normal file
34
apps/app/components/issues/peek-overview/issue-details.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// components
|
||||||
|
import { IssueDescriptionForm, IssueReaction } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleUpdateIssue: (formData: Partial<IIssue>) => Promise<void>;
|
||||||
|
issue: IIssue;
|
||||||
|
readOnly: boolean;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PeekOverviewIssueDetails: React.FC<Props> = ({
|
||||||
|
handleUpdateIssue,
|
||||||
|
issue,
|
||||||
|
readOnly,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h6 className="font-medium text-custom-text-200">
|
||||||
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
|
</h6>
|
||||||
|
<IssueDescriptionForm
|
||||||
|
handleFormSubmit={handleUpdateIssue}
|
||||||
|
isAllowed={!readOnly}
|
||||||
|
issue={{
|
||||||
|
name: issue.name,
|
||||||
|
description_html: issue.description_html,
|
||||||
|
}}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
<IssueReaction workspaceSlug={workspaceSlug} issueId={issue.id} projectId={issue.project} />
|
||||||
|
</div>
|
||||||
|
);
|
203
apps/app/components/issues/peek-overview/issue-properties.tsx
Normal file
203
apps/app/components/issues/peek-overview/issue-properties.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// headless ui
|
||||||
|
import { Disclosure } from "@headlessui/react";
|
||||||
|
import { getStateGroupIcon } from "components/icons";
|
||||||
|
// components
|
||||||
|
import {
|
||||||
|
SidebarAssigneeSelect,
|
||||||
|
SidebarEstimateSelect,
|
||||||
|
SidebarPrioritySelect,
|
||||||
|
SidebarStateSelect,
|
||||||
|
TPeekOverviewModes,
|
||||||
|
} from "components/issues";
|
||||||
|
// icons
|
||||||
|
import { CustomDatePicker, Icon } from "components/ui";
|
||||||
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleDeleteIssue: () => void;
|
||||||
|
issue: IIssue;
|
||||||
|
mode: TPeekOverviewModes;
|
||||||
|
onChange: (issueProperty: Partial<IIssue>) => void;
|
||||||
|
readOnly: boolean;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PeekOverviewIssueProperties: React.FC<Props> = ({
|
||||||
|
handleDeleteIssue,
|
||||||
|
issue,
|
||||||
|
mode,
|
||||||
|
onChange,
|
||||||
|
readOnly,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => {
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const startDate = issue.start_date;
|
||||||
|
const targetDate = issue.target_date;
|
||||||
|
|
||||||
|
const minDate = startDate ? new Date(startDate) : null;
|
||||||
|
minDate?.setDate(minDate.getDate());
|
||||||
|
|
||||||
|
const maxDate = targetDate ? new Date(targetDate) : null;
|
||||||
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
|
const handleCopyLink = () => {
|
||||||
|
const originURL =
|
||||||
|
typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
|
||||||
|
copyTextToClipboard(
|
||||||
|
`${originURL}/${workspaceSlug}/projects/${issue.project}/issues/${issue.id}`
|
||||||
|
).then(() => {
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link copied!",
|
||||||
|
message: "Issue link copied to clipboard",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mode === "full" ? "divide-y divide-custom-border-200" : ""}>
|
||||||
|
{mode === "full" && (
|
||||||
|
<div className="flex justify-between gap-2 pb-3">
|
||||||
|
<h6 className="flex items-center gap-2 font-medium">
|
||||||
|
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)}
|
||||||
|
{issue.project_detail.identifier}-{issue.sequence_id}
|
||||||
|
</h6>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button type="button" onClick={handleCopyLink} className="-rotate-45">
|
||||||
|
<Icon iconName="link" />
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={handleDeleteIssue}>
|
||||||
|
<Icon iconName="delete" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`space-y-4 ${mode === "full" ? "pt-3" : ""}`}>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="radio_button_checked" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">State</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-3/4">
|
||||||
|
<SidebarStateSelect
|
||||||
|
value={issue.state}
|
||||||
|
onChange={(val: string) => onChange({ state: val })}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="group" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">Assignees</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-3/4">
|
||||||
|
<SidebarAssigneeSelect
|
||||||
|
value={issue.assignees_list}
|
||||||
|
onChange={(val: string[]) => onChange({ assignees_list: val })}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="signal_cellular_alt" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">Priority</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-3/4">
|
||||||
|
<SidebarPrioritySelect
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={(val: string) => onChange({ priority: val })}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">Start date</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{issue.start_date ? (
|
||||||
|
<CustomDatePicker
|
||||||
|
placeholder="Start date"
|
||||||
|
value={issue.start_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
onChange({
|
||||||
|
start_date: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="bg-custom-background-100"
|
||||||
|
wrapperClassName="w-full"
|
||||||
|
maxDate={maxDate ?? undefined}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-custom-text-200">Empty</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="calendar_today" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">Due date</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{issue.target_date ? (
|
||||||
|
<CustomDatePicker
|
||||||
|
placeholder="Due date"
|
||||||
|
value={issue.target_date}
|
||||||
|
onChange={(val) =>
|
||||||
|
onChange({
|
||||||
|
target_date: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="bg-custom-background-100"
|
||||||
|
wrapperClassName="w-full"
|
||||||
|
minDate={minDate ?? undefined}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-custom-text-200">Empty</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex-shrink-0 w-1/4 flex items-center gap-2 font-medium">
|
||||||
|
<Icon iconName="change_history" className="!text-base flex-shrink-0" />
|
||||||
|
<span className="flex-grow truncate">Estimate</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-3/4">
|
||||||
|
<SidebarEstimateSelect
|
||||||
|
value={issue.estimate_point}
|
||||||
|
onChange={(val: number | null) => onChange({ estimate_point: val })}
|
||||||
|
disabled={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
{/* <Disclosure as="div">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Disclosure.Button
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 text-sm text-custom-text-200"
|
||||||
|
>
|
||||||
|
Show {open ? "Less" : "More"}
|
||||||
|
<Icon iconName={open ? "expand_less" : "expand_more"} className="!text-base" />
|
||||||
|
</Disclosure.Button>
|
||||||
|
<Disclosure.Panel as="div" className="mt-4 space-y-4">
|
||||||
|
Disclosure Panel
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
107
apps/app/components/issues/peek-overview/layout.tsx
Normal file
107
apps/app/components/issues/peek-overview/layout.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { FullScreenPeekView, SidePeekView } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleDeleteIssue: () => void;
|
||||||
|
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||||
|
issue: IIssue | null;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
workspaceSlug: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPeekOverviewModes = "side" | "modal" | "full";
|
||||||
|
|
||||||
|
export const IssuePeekOverview: React.FC<Props> = ({
|
||||||
|
handleDeleteIssue,
|
||||||
|
handleUpdateIssue,
|
||||||
|
issue,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
workspaceSlug,
|
||||||
|
readOnly,
|
||||||
|
}) => {
|
||||||
|
const [peekOverviewMode, setPeekOverviewMode] = useState<TPeekOverviewModes>("side");
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setPeekOverviewMode("side");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!issue || !isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
|
{/* add backdrop conditionally */}
|
||||||
|
{(peekOverviewMode === "modal" || peekOverviewMode === "full") && (
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
)}
|
||||||
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
<Transition.Child
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel
|
||||||
|
className={`absolute z-20 bg-custom-background-100 ${
|
||||||
|
peekOverviewMode === "side"
|
||||||
|
? "top-0 right-0 h-full w-1/2 shadow-custom-shadow-md"
|
||||||
|
: peekOverviewMode === "modal"
|
||||||
|
? "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[70%] w-3/5 rounded-lg shadow-custom-shadow-xl"
|
||||||
|
: "top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[95%] w-[95%] rounded-lg shadow-custom-shadow-xl"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{(peekOverviewMode === "side" || peekOverviewMode === "modal") && (
|
||||||
|
<SidePeekView
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode={peekOverviewMode}
|
||||||
|
readOnly={readOnly}
|
||||||
|
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{peekOverviewMode === "full" && (
|
||||||
|
<FullScreenPeekView
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode={peekOverviewMode}
|
||||||
|
readOnly={readOnly}
|
||||||
|
setMode={(mode) => setPeekOverviewMode(mode)}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
};
|
75
apps/app/components/issues/peek-overview/side-peek-view.tsx
Normal file
75
apps/app/components/issues/peek-overview/side-peek-view.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
PeekOverviewHeader,
|
||||||
|
PeekOverviewIssueActivity,
|
||||||
|
PeekOverviewIssueDetails,
|
||||||
|
PeekOverviewIssueProperties,
|
||||||
|
TPeekOverviewModes,
|
||||||
|
} from "components/issues";
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleClose: () => void;
|
||||||
|
handleDeleteIssue: () => void;
|
||||||
|
handleUpdateIssue: (issue: Partial<IIssue>) => Promise<void>;
|
||||||
|
issue: IIssue;
|
||||||
|
mode: TPeekOverviewModes;
|
||||||
|
readOnly: boolean;
|
||||||
|
setMode: (mode: TPeekOverviewModes) => void;
|
||||||
|
workspaceSlug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidePeekView: React.FC<Props> = ({
|
||||||
|
handleClose,
|
||||||
|
handleDeleteIssue,
|
||||||
|
handleUpdateIssue,
|
||||||
|
issue,
|
||||||
|
mode,
|
||||||
|
readOnly,
|
||||||
|
setMode,
|
||||||
|
workspaceSlug,
|
||||||
|
}) => (
|
||||||
|
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||||
|
<div className="w-full p-5">
|
||||||
|
<PeekOverviewHeader
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode={mode}
|
||||||
|
setMode={setMode}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-full px-6 overflow-y-auto">
|
||||||
|
{/* issue title and description */}
|
||||||
|
<div className="w-full">
|
||||||
|
<PeekOverviewIssueDetails
|
||||||
|
handleUpdateIssue={handleUpdateIssue}
|
||||||
|
issue={issue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* issue properties */}
|
||||||
|
<div className="w-full mt-10">
|
||||||
|
<PeekOverviewIssueProperties
|
||||||
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
|
issue={issue}
|
||||||
|
mode={mode}
|
||||||
|
onChange={handleUpdateIssue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* divider */}
|
||||||
|
<div className="h-[1] w-full border-t border-custom-border-200 my-5" />
|
||||||
|
{/* issue activity/comments */}
|
||||||
|
<div className="w-full pb-5">
|
||||||
|
<PeekOverviewIssueActivity
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
issue={issue}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
@ -9,26 +9,16 @@ import projectService from "services/project.service";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomSearchSelect } from "components/ui";
|
import { CustomSearchSelect } from "components/ui";
|
||||||
import { AssigneesList, Avatar } from "components/ui/avatar";
|
import { AssigneesList, Avatar } from "components/ui/avatar";
|
||||||
// icons
|
|
||||||
import { UserGroupIcon } from "@heroicons/react/24/outline";
|
|
||||||
// types
|
|
||||||
import { UserAuth } from "types";
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (val: string[]) => void;
|
onChange: (val: string[]) => void;
|
||||||
userAuth: UserAuth;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarAssigneeSelect: React.FC<Props> = ({
|
export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
userAuth,
|
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -50,36 +40,27 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<CustomSearchSelect
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
value={value}
|
||||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
customButton={
|
||||||
<p>Assignees</p>
|
<>
|
||||||
</div>
|
{value && value.length > 0 && Array.isArray(value) ? (
|
||||||
<div className="sm:basis-1/2">
|
<div className="-my-0.5 flex items-center gap-2">
|
||||||
<CustomSearchSelect
|
<AssigneesList userIds={value} length={3} showLength={false} />
|
||||||
value={value}
|
<span className="text-custom-text-100 text-sm">{value.length} Assignees</span>
|
||||||
label={
|
|
||||||
<div className="flex items-center gap-2 text-custom-text-200">
|
|
||||||
{value && value.length > 0 && Array.isArray(value) ? (
|
|
||||||
<div className="-my-0.5 flex items-center justify-center gap-2">
|
|
||||||
<AssigneesList userIds={value} length={3} showLength={false} />
|
|
||||||
<span className="text-custom-text-100">{value.length} Assignees</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"No assignees"
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
) : (
|
||||||
options={options}
|
<button type="button" className="bg-custom-background-80 px-2.5 py-0.5 text-sm rounded">
|
||||||
onChange={onChange}
|
No assignees
|
||||||
position="right"
|
</button>
|
||||||
multiple
|
)}
|
||||||
disabled={isNotAllowed}
|
</>
|
||||||
/>
|
}
|
||||||
</div>
|
options={options}
|
||||||
</div>
|
onChange={onChange}
|
||||||
|
multiple
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,75 +6,56 @@ import useEstimateOption from "hooks/use-estimate-option";
|
|||||||
import { CustomSelect } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { PlayIcon } from "@heroicons/react/24/outline";
|
import { PlayIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
|
||||||
import { UserAuth } from "types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
onChange: (val: number | null) => void;
|
onChange: (val: number | null) => void;
|
||||||
userAuth: UserAuth;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarEstimateSelect: React.FC<Props> = ({
|
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
userAuth,
|
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
|
||||||
|
|
||||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||||
|
|
||||||
if (!isEstimateActive) return null;
|
if (!isEstimateActive) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<CustomSelect
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
value={value}
|
||||||
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
customButton={
|
||||||
<p>Estimate</p>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
<div className="sm:basis-1/2">
|
className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5"
|
||||||
<CustomSelect
|
|
||||||
value={value}
|
|
||||||
label={
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<PlayIcon
|
|
||||||
className={`h-4 w-4 -rotate-90 ${
|
|
||||||
value !== null ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{estimatePoints?.find((e) => e.key === value)?.value ?? (
|
|
||||||
<span className="text-custom-text-200">No estimates</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onChange={onChange}
|
|
||||||
position="right"
|
|
||||||
width="w-full"
|
|
||||||
disabled={isNotAllowed || disabled}
|
|
||||||
>
|
>
|
||||||
<CustomSelect.Option value={null}>
|
<PlayIcon
|
||||||
|
className={`h-4 w-4 -rotate-90 ${
|
||||||
|
value !== null ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<CustomSelect.Option value={null}>
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||||
|
</span>
|
||||||
|
None
|
||||||
|
</>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
{estimatePoints &&
|
||||||
|
estimatePoints.map((point) => (
|
||||||
|
<CustomSelect.Option key={point.key} value={point.key}>
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
<PlayIcon className="h-4 w-4 -rotate-90" />
|
||||||
</span>
|
</span>
|
||||||
None
|
{point.value}
|
||||||
</>
|
</>
|
||||||
</CustomSelect.Option>
|
</CustomSelect.Option>
|
||||||
{estimatePoints &&
|
))}
|
||||||
estimatePoints.map((point) => (
|
</CustomSelect>
|
||||||
<CustomSelect.Option key={point.key} value={point.key}>
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
<PlayIcon className="h-4 w-4 -rotate-90" />
|
|
||||||
</span>
|
|
||||||
{point.value}
|
|
||||||
</>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,62 +3,51 @@ import React from "react";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomSelect } from "components/ui";
|
import { CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { ChartBarIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { getPriorityIcon } from "components/icons/priority-icon";
|
import { getPriorityIcon } from "components/icons/priority-icon";
|
||||||
// types
|
|
||||||
import { UserAuth } from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string | null;
|
value: string | null;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
userAuth: UserAuth;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarPrioritySelect: React.FC<Props> = ({
|
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabled = false }) => (
|
||||||
value,
|
<CustomSelect
|
||||||
onChange,
|
customButton={
|
||||||
userAuth,
|
<button
|
||||||
disabled = false,
|
type="button"
|
||||||
}) => {
|
className={`flex items-center gap-1.5 text-left text-sm capitalize rounded px-2.5 py-0.5 ${
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
value === "urgent"
|
||||||
|
? "border-red-500/20 bg-red-500/20 text-red-500"
|
||||||
return (
|
: value === "high"
|
||||||
<div className="flex flex-wrap items-center py-2">
|
? "border-orange-500/20 bg-orange-500/20 text-orange-500"
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
: value === "medium"
|
||||||
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
|
? "border-yellow-500/20 bg-yellow-500/20 text-yellow-500"
|
||||||
<p>Priority</p>
|
: value === "low"
|
||||||
</div>
|
? "border-green-500/20 bg-green-500/20 text-green-500"
|
||||||
<div className="sm:basis-1/2">
|
: "bg-custom-background-80 border-custom-border-200"
|
||||||
<CustomSelect
|
}`}
|
||||||
label={
|
>
|
||||||
<div className="flex items-center gap-2 text-left capitalize">
|
<span className="grid place-items-center -my-1">
|
||||||
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
{getPriorityIcon(value ?? "None", "!text-sm")}
|
||||||
{getPriorityIcon(value ?? "None", "text-sm")}
|
</span>
|
||||||
</span>
|
<span>{value ?? "None"}</span>
|
||||||
<span className={`${value ? "text-custom-text-100" : "text-custom-text-200"}`}>
|
</button>
|
||||||
{value ?? "None"}
|
}
|
||||||
</span>
|
value={value}
|
||||||
</div>
|
onChange={onChange}
|
||||||
}
|
optionsClassName="w-min"
|
||||||
value={value}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
>
|
||||||
width="w-full"
|
{PRIORITIES.map((option) => (
|
||||||
position="right"
|
<CustomSelect.Option key={option} value={option} className="capitalize">
|
||||||
disabled={isNotAllowed}
|
<>
|
||||||
>
|
{getPriorityIcon(option, "text-sm")}
|
||||||
{PRIORITIES.map((option) => (
|
{option ?? "None"}
|
||||||
<CustomSelect.Option key={option} value={option} className="capitalize">
|
</>
|
||||||
<>
|
</CustomSelect.Option>
|
||||||
{getPriorityIcon(option, "text-sm")}
|
))}
|
||||||
{option ?? "None"}
|
</CustomSelect>
|
||||||
</>
|
);
|
||||||
</CustomSelect.Option>
|
|
||||||
))}
|
|
||||||
</CustomSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -9,29 +9,20 @@ import stateService from "services/state.service";
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner, CustomSelect } from "components/ui";
|
import { Spinner, CustomSelect } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Squares2X2Icon } from "@heroicons/react/24/outline";
|
|
||||||
import { getStateGroupIcon } from "components/icons";
|
import { getStateGroupIcon } from "components/icons";
|
||||||
// helpers
|
// helpers
|
||||||
import { getStatesList } from "helpers/state.helper";
|
import { getStatesList } from "helpers/state.helper";
|
||||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
|
||||||
import { UserAuth } from "types";
|
|
||||||
// constants
|
// constants
|
||||||
import { STATES_LIST } from "constants/fetch-keys";
|
import { STATES_LIST } from "constants/fetch-keys";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
userAuth: UserAuth;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarStateSelect: React.FC<Props> = ({
|
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
userAuth,
|
|
||||||
disabled = false,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
||||||
|
|
||||||
@ -45,60 +36,52 @@ export const SidebarStateSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const selectedState = states?.find((s) => s.id === value);
|
const selectedState = states?.find((s) => s.id === value);
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<CustomSelect
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
customButton={
|
||||||
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
<button type="button" className="bg-custom-background-80 text-sm rounded px-2.5 py-0.5">
|
||||||
<p>State</p>
|
{selectedState ? (
|
||||||
</div>
|
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||||
<div className="sm:basis-1/2">
|
{getStateGroupIcon(
|
||||||
<CustomSelect
|
selectedState?.group ?? "backlog",
|
||||||
label={
|
"14",
|
||||||
selectedState ? (
|
"14",
|
||||||
<div className="flex items-center gap-2 text-left text-custom-text-100">
|
selectedState?.color ?? ""
|
||||||
{getStateGroupIcon(
|
)}
|
||||||
selectedState?.group ?? "backlog",
|
{addSpaceIfCamelCase(selectedState?.name ?? "")}
|
||||||
"16",
|
</div>
|
||||||
"16",
|
) : inboxIssueId ? (
|
||||||
selectedState?.color ?? ""
|
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
|
||||||
)}
|
{getStateGroupIcon("backlog", "14", "14", "#ff7700")}
|
||||||
{addSpaceIfCamelCase(selectedState?.name ?? "")}
|
Triage
|
||||||
</div>
|
</div>
|
||||||
) : inboxIssueId ? (
|
|
||||||
<div className="flex items-center gap-2 text-left text-custom-text-100">
|
|
||||||
{getStateGroupIcon("backlog", "16", "16", "#ff7700")}
|
|
||||||
Triage
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"None"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
width="w-full"
|
|
||||||
position="right"
|
|
||||||
disabled={isNotAllowed}
|
|
||||||
>
|
|
||||||
{states ? (
|
|
||||||
states.length > 0 ? (
|
|
||||||
states.map((state) => (
|
|
||||||
<CustomSelect.Option key={state.id} value={state.id}>
|
|
||||||
<>
|
|
||||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
|
||||||
{state.name}
|
|
||||||
</>
|
|
||||||
</CustomSelect.Option>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-center">No states found</div>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Spinner />
|
"None"
|
||||||
)}
|
)}
|
||||||
</CustomSelect>
|
</button>
|
||||||
</div>
|
}
|
||||||
</div>
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
optionsClassName="w-min"
|
||||||
|
position="left"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{states ? (
|
||||||
|
states.length > 0 ? (
|
||||||
|
states.map((state) => (
|
||||||
|
<CustomSelect.Option key={state.id} value={state.id}>
|
||||||
|
<>
|
||||||
|
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||||
|
{state.name}
|
||||||
|
</>
|
||||||
|
</CustomSelect.Option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-center">No states found</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</CustomSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,16 @@ import {
|
|||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker, Icon } from "components/ui";
|
import { CustomDatePicker, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { LinkIcon, CalendarDaysIcon, TrashIcon, PlusIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
LinkIcon,
|
||||||
|
CalendarDaysIcon,
|
||||||
|
TrashIcon,
|
||||||
|
PlusIcon,
|
||||||
|
Squares2X2Icon,
|
||||||
|
ChartBarIcon,
|
||||||
|
UserGroupIcon,
|
||||||
|
PlayIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -332,60 +341,90 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
{showFirstSection && (
|
{showFirstSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
<Controller
|
<div className="flex flex-wrap items-center py-2">
|
||||||
control={control}
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
name="state"
|
<Squares2X2Icon className="h-4 w-4 flex-shrink-0" />
|
||||||
render={({ field: { value } }) => (
|
<p>State</p>
|
||||||
<SidebarStateSelect
|
</div>
|
||||||
value={value}
|
<div className="sm:basis-1/2">
|
||||||
onChange={(val: string) => submitChanges({ state: val })}
|
<Controller
|
||||||
userAuth={memberRole}
|
control={control}
|
||||||
disabled={uneditable}
|
name="state"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<SidebarStateSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => submitChanges({ state: val })}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||||
<Controller
|
<div className="flex flex-wrap items-center py-2">
|
||||||
control={control}
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
name="assignees_list"
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
render={({ field: { value } }) => (
|
<p>Assignees</p>
|
||||||
<SidebarAssigneeSelect
|
</div>
|
||||||
value={value}
|
<div className="sm:basis-1/2">
|
||||||
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
<Controller
|
||||||
userAuth={memberRole}
|
control={control}
|
||||||
disabled={uneditable}
|
name="assignees_list"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<SidebarAssigneeSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||||
<Controller
|
<div className="flex flex-wrap items-center py-2">
|
||||||
control={control}
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
name="priority"
|
<ChartBarIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
render={({ field: { value } }) => (
|
<p>Priority</p>
|
||||||
<SidebarPrioritySelect
|
</div>
|
||||||
value={value}
|
<div className="sm:basis-1/2">
|
||||||
onChange={(val: string) => submitChanges({ priority: val })}
|
<Controller
|
||||||
userAuth={memberRole}
|
control={control}
|
||||||
disabled={uneditable}
|
name="priority"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<SidebarPrioritySelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: string) => submitChanges({ priority: val })}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && (
|
||||||
<Controller
|
<div className="flex flex-wrap items-center py-2">
|
||||||
control={control}
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
name="estimate_point"
|
<PlayIcon className="h-4 w-4 flex-shrink-0 -rotate-90" />
|
||||||
render={({ field: { value } }) => (
|
<p>Estimate</p>
|
||||||
<SidebarEstimateSelect
|
</div>
|
||||||
value={value}
|
<div className="sm:basis-1/2">
|
||||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
<Controller
|
||||||
userAuth={memberRole}
|
control={control}
|
||||||
disabled={uneditable}
|
name="estimate_point"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<SidebarEstimateSelect
|
||||||
|
value={value}
|
||||||
|
onChange={(val: number | null) =>
|
||||||
|
submitChanges({ estimate_point: val })
|
||||||
|
}
|
||||||
|
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -115,14 +115,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (key === "target_date") {
|
if (key === "start_date" || key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
||||||
filters?.target_date ?? [],
|
|
||||||
option.value
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters({
|
setFilters({
|
||||||
target_date: valueExists ? null : option.value,
|
[key]: valueExists ? null : option.value,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const valueExists = filters[key]?.includes(option.value);
|
const valueExists = filters[key]?.includes(option.value);
|
||||||
@ -186,7 +183,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{GROUP_BY_OPTIONS.map((option) => {
|
{GROUP_BY_OPTIONS.map((option) => {
|
||||||
if (issueView === "kanban" && option.key === null) return null;
|
if (issueView === "kanban" && option.key === null) return null;
|
||||||
if (option.key === "state" || option.key === "created_by")
|
if (
|
||||||
|
option.key === "state" ||
|
||||||
|
option.key === "created_by" ||
|
||||||
|
option.key === "assignees"
|
||||||
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -263,6 +263,7 @@ export const ProfileIssuesView = () => {
|
|||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state_group: null,
|
state_group: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
type: null,
|
||||||
})
|
})
|
||||||
|
@ -42,7 +42,7 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
|||||||
user,
|
user,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -81,6 +81,8 @@ export const DeleteProjectModal: React.FC<TConfirmProjectDeletionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
|
|
||||||
|
if (projectId && projectId === data.id) router.push(`/${workspaceSlug}/projects`);
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
|||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
import { useForm, Controller, useFieldArray } from "react-hook-form";
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_MEMBERS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { ROLE } from "constants/workspace";
|
import { ROLE } from "constants/workspace";
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ type Props = {
|
|||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
members: any[];
|
members: any[];
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
onSuccess: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type member = {
|
type member = {
|
||||||
@ -57,7 +58,9 @@ const defaultValues: FormValues = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, members, user }) => {
|
const SendProjectInvitationModal: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen, setIsOpen, members, user, onSuccess } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -98,13 +101,13 @@ const SendProjectInvitationModal: React.FC<Props> = ({ isOpen, setIsOpen, member
|
|||||||
type: "success",
|
type: "success",
|
||||||
message: "Member added successfully",
|
message: "Member added successfully",
|
||||||
});
|
});
|
||||||
|
onSuccess();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
reset(defaultValues);
|
reset(defaultValues);
|
||||||
mutate(PROJECT_MEMBERS(projectId.toString()));
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -73,9 +73,10 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||||
${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||||
} ${customClassName}`;
|
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||||
|
} ${customClassName}`;
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
@ -94,6 +94,7 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
labels: null,
|
labels: null,
|
||||||
priority: null,
|
priority: null,
|
||||||
state: null,
|
state: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
type: null,
|
||||||
});
|
});
|
||||||
@ -155,14 +156,15 @@ export const ViewForm: React.FC<Props> = ({
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
const key = option.key as keyof typeof filters;
|
const key = option.key as keyof typeof filters;
|
||||||
|
|
||||||
if (key === "target_date") {
|
if (key === "start_date" || key === "target_date") {
|
||||||
const valueExists = checkIfArraysHaveSameElements(
|
const valueExists = checkIfArraysHaveSameElements(
|
||||||
filters?.target_date ?? [],
|
filters?.[key] ?? [],
|
||||||
option.value
|
option.value
|
||||||
);
|
);
|
||||||
|
|
||||||
setValue("query", {
|
setValue("query", {
|
||||||
target_date: valueExists ? null : option.value,
|
...filters,
|
||||||
|
[key]: valueExists ? null : option.value,
|
||||||
} as IQuery);
|
} as IQuery);
|
||||||
} else {
|
} else {
|
||||||
if (!filters?.[key]?.includes(option.value))
|
if (!filters?.[key]?.includes(option.value))
|
||||||
|
@ -9,7 +9,7 @@ import stateService from "services/state.service";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import issuesService from "services/issues.service";
|
import issuesService from "services/issues.service";
|
||||||
// components
|
// components
|
||||||
import { DueDateFilterModal } from "components/core";
|
import { DateFilterModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, MultiLevelDropdown } from "components/ui";
|
import { Avatar, MultiLevelDropdown } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -23,7 +23,7 @@ import { IIssueFilterOptions, IQuery } from "types";
|
|||||||
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS, STATES_LIST } from "constants/fetch-keys";
|
||||||
// constants
|
// constants
|
||||||
import { PRIORITIES } from "constants/project";
|
import { PRIORITIES } from "constants/project";
|
||||||
import { DUE_DATES } from "constants/due-dates";
|
import { DATE_FILTER_OPTIONS } from "constants/filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filters: Partial<IIssueFilterOptions> | IQuery;
|
filters: Partial<IIssueFilterOptions> | IQuery;
|
||||||
@ -38,7 +38,14 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
direction = "right",
|
direction = "right",
|
||||||
height = "md",
|
height = "md",
|
||||||
}) => {
|
}) => {
|
||||||
const [isDueDateFilterModalOpen, setIsDueDateFilterModalOpen] = useState(false);
|
const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false);
|
||||||
|
const [dateFilterType, setDateFilterType] = useState<{
|
||||||
|
title: string;
|
||||||
|
type: "start_date" | "target_date";
|
||||||
|
}>({
|
||||||
|
title: "",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
@ -67,10 +74,12 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDueDateFilterModalOpen && (
|
{isDateFilterModalOpen && (
|
||||||
<DueDateFilterModal
|
<DateFilterModal
|
||||||
isOpen={isDueDateFilterModalOpen}
|
title={dateFilterType.title}
|
||||||
handleClose={() => setIsDueDateFilterModalOpen(false)}
|
field={dateFilterType.type}
|
||||||
|
isOpen={isDateFilterModalOpen}
|
||||||
|
handleClose={() => setIsDateFilterModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MultiLevelDropdown
|
<MultiLevelDropdown
|
||||||
@ -183,12 +192,48 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "target_date",
|
id: "start_date",
|
||||||
label: "Due date",
|
label: "Start date",
|
||||||
value: DUE_DATES,
|
value: DATE_FILTER_OPTIONS,
|
||||||
hasChildren: true,
|
hasChildren: true,
|
||||||
children: [
|
children: [
|
||||||
...DUE_DATES.map((option) => ({
|
...DATE_FILTER_OPTIONS.map((option) => ({
|
||||||
|
id: option.name,
|
||||||
|
label: option.name,
|
||||||
|
value: {
|
||||||
|
key: "start_date",
|
||||||
|
value: option.value,
|
||||||
|
},
|
||||||
|
selected: checkIfArraysHaveSameElements(filters?.start_date ?? [], option.value),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
label: "Custom",
|
||||||
|
value: "custom",
|
||||||
|
element: (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Start date",
|
||||||
|
type: "start_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "target_date",
|
||||||
|
label: "Due date",
|
||||||
|
value: DATE_FILTER_OPTIONS,
|
||||||
|
hasChildren: true,
|
||||||
|
children: [
|
||||||
|
...DATE_FILTER_OPTIONS.map((option) => ({
|
||||||
id: option.name,
|
id: option.name,
|
||||||
label: option.name,
|
label: option.name,
|
||||||
value: {
|
value: {
|
||||||
@ -203,7 +248,13 @@ export const SelectFilters: React.FC<Props> = ({
|
|||||||
value: "custom",
|
value: "custom",
|
||||||
element: (
|
element: (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsDueDateFilterModalOpen(true)}
|
onClick={() => {
|
||||||
|
setIsDateFilterModalOpen(true);
|
||||||
|
setDateFilterType({
|
||||||
|
title: "Due date",
|
||||||
|
type: "target_date",
|
||||||
|
});
|
||||||
|
}}
|
||||||
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
className="w-full rounded px-1 py-1.5 text-left text-custom-text-200 hover:bg-custom-background-80"
|
||||||
>
|
>
|
||||||
Custom
|
Custom
|
||||||
|
@ -25,6 +25,7 @@ type Props = {
|
|||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
workspace_slug: string;
|
workspace_slug: string;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
onSuccess: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EmailRole = {
|
type EmailRole = {
|
||||||
@ -45,12 +46,8 @@ const defaultValues: FormValues = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const SendWorkspaceInvitationModal: React.FC<Props> = ({
|
const SendWorkspaceInvitationModal: React.FC<Props> = (props) => {
|
||||||
isOpen,
|
const { isOpen, setIsOpen, workspace_slug, user, onSuccess } = props;
|
||||||
setIsOpen,
|
|
||||||
workspace_slug,
|
|
||||||
user,
|
|
||||||
}) => {
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
reset,
|
reset,
|
||||||
@ -88,6 +85,7 @@ const SendWorkspaceInvitationModal: React.FC<Props> = ({
|
|||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Invitations sent successfully.",
|
message: "Invitations sent successfully.",
|
||||||
});
|
});
|
||||||
|
onSuccess();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -8,6 +8,7 @@ const paramsToKey = (params: any) => {
|
|||||||
assignees,
|
assignees,
|
||||||
created_by,
|
created_by,
|
||||||
labels,
|
labels,
|
||||||
|
start_date,
|
||||||
target_date,
|
target_date,
|
||||||
sub_issue,
|
sub_issue,
|
||||||
start_target_date,
|
start_target_date,
|
||||||
@ -19,6 +20,7 @@ const paramsToKey = (params: any) => {
|
|||||||
let createdByKey = created_by ? created_by.split(",") : [];
|
let createdByKey = created_by ? created_by.split(",") : [];
|
||||||
let labelsKey = labels ? labels.split(",") : [];
|
let labelsKey = labels ? labels.split(",") : [];
|
||||||
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
|
const startTargetDate = start_target_date ? `${start_target_date}`.toUpperCase() : "FALSE";
|
||||||
|
const startDateKey = start_date ?? "";
|
||||||
const targetDateKey = target_date ?? "";
|
const targetDateKey = target_date ?? "";
|
||||||
const type = params.type ? params.type.toUpperCase() : "NULL";
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
@ -31,7 +33,7 @@ const paramsToKey = (params: any) => {
|
|||||||
createdByKey = createdByKey.sort().join("_");
|
createdByKey = createdByKey.sort().join("_");
|
||||||
labelsKey = labelsKey.sort().join("_");
|
labelsKey = labelsKey.sort().join("_");
|
||||||
|
|
||||||
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
|
return `${stateKey}_${priorityKey}_${assigneesKey}_${createdByKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}_${sub_issue}_${startTargetDate}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inboxParamsToKey = (params: any) => {
|
const inboxParamsToKey = (params: any) => {
|
||||||
@ -48,7 +50,16 @@ const inboxParamsToKey = (params: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const myIssuesParamsToKey = (params: any) => {
|
const myIssuesParamsToKey = (params: any) => {
|
||||||
const { assignees, created_by, labels, priority, state_group, subscriber, target_date } = params;
|
const {
|
||||||
|
assignees,
|
||||||
|
created_by,
|
||||||
|
labels,
|
||||||
|
priority,
|
||||||
|
state_group,
|
||||||
|
subscriber,
|
||||||
|
start_date,
|
||||||
|
target_date,
|
||||||
|
} = params;
|
||||||
|
|
||||||
let assigneesKey = assignees ? assignees.split(",") : [];
|
let assigneesKey = assignees ? assignees.split(",") : [];
|
||||||
let createdByKey = created_by ? created_by.split(",") : [];
|
let createdByKey = created_by ? created_by.split(",") : [];
|
||||||
@ -56,6 +67,7 @@ const myIssuesParamsToKey = (params: any) => {
|
|||||||
let subscriberKey = subscriber ? subscriber.split(",") : [];
|
let subscriberKey = subscriber ? subscriber.split(",") : [];
|
||||||
let priorityKey = priority ? priority.split(",") : [];
|
let priorityKey = priority ? priority.split(",") : [];
|
||||||
let labelsKey = labels ? labels.split(",") : [];
|
let labelsKey = labels ? labels.split(",") : [];
|
||||||
|
const startDateKey = start_date ?? "";
|
||||||
const targetDateKey = target_date ?? "";
|
const targetDateKey = target_date ?? "";
|
||||||
const type = params.type ? params.type.toUpperCase() : "NULL";
|
const type = params.type ? params.type.toUpperCase() : "NULL";
|
||||||
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
const groupBy = params.group_by ? params.group_by.toUpperCase() : "NULL";
|
||||||
@ -69,7 +81,7 @@ const myIssuesParamsToKey = (params: any) => {
|
|||||||
priorityKey = priorityKey.sort().join("_");
|
priorityKey = priorityKey.sort().join("_");
|
||||||
labelsKey = labelsKey.sort().join("_");
|
labelsKey = labelsKey.sort().join("_");
|
||||||
|
|
||||||
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${targetDateKey}`;
|
return `${assigneesKey}_${createdByKey}_${stateGroupKey}_${subscriberKey}_${priorityKey}_${type}_${groupBy}_${orderBy}_${labelsKey}_${startDateKey}_${targetDateKey}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CURRENT_USER = "CURRENT_USER";
|
export const CURRENT_USER = "CURRENT_USER";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// helper
|
// helper
|
||||||
import { renderDateFormat } from "helpers/date-time.helper";
|
import { renderDateFormat } from "helpers/date-time.helper";
|
||||||
|
|
||||||
export const DUE_DATES = [
|
export const DATE_FILTER_OPTIONS = [
|
||||||
{
|
{
|
||||||
name: "Last week",
|
name: "Last week",
|
||||||
value: [
|
value: [
|
@ -7,6 +7,7 @@ export const GROUP_BY_OPTIONS: Array<{
|
|||||||
{ name: "Priority", key: "priority" },
|
{ name: "Priority", key: "priority" },
|
||||||
{ name: "Project", key: "project" },
|
{ name: "Project", key: "project" },
|
||||||
{ name: "Labels", key: "labels" },
|
{ name: "Labels", key: "labels" },
|
||||||
|
{ name: "Assignees", key: "assignees" },
|
||||||
{ name: "Created by", key: "created_by" },
|
{ name: "Created by", key: "created_by" },
|
||||||
{ name: "None", key: null },
|
{ name: "None", key: null },
|
||||||
];
|
];
|
||||||
|
@ -94,6 +94,7 @@ export const initialState: StateType = {
|
|||||||
state_group: null,
|
state_group: null,
|
||||||
subscriber: null,
|
subscriber: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -72,6 +72,7 @@ export const initialState: StateType = {
|
|||||||
state_group: null,
|
state_group: null,
|
||||||
subscriber: null,
|
subscriber: null,
|
||||||
created_by: null,
|
created_by: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -26,6 +26,7 @@ const initialValues: IWorkspaceViewProps = {
|
|||||||
priority: null,
|
priority: null,
|
||||||
state_group: null,
|
state_group: null,
|
||||||
subscriber: null,
|
subscriber: null,
|
||||||
|
start_date: null,
|
||||||
target_date: null,
|
target_date: null,
|
||||||
type: null,
|
type: null,
|
||||||
},
|
},
|
||||||
|
@ -27,6 +27,7 @@ const useMyIssues = (workspaceSlug: string | undefined) => {
|
|||||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
|
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
|
||||||
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
|
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
|
||||||
|
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
|
||||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,7 @@ const useIssuesView = () => {
|
|||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
labels: filters?.labels ? filters?.labels.join(",") : undefined,
|
||||||
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
|
||||||
|
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
|
||||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||||
sub_issue: showSubIssues,
|
sub_issue: showSubIssues,
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ const useProfileIssues = (workspaceSlug: string | undefined, userId: string | un
|
|||||||
order_by: orderBy,
|
order_by: orderBy,
|
||||||
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
priority: filters?.priority ? filters?.priority.join(",") : undefined,
|
||||||
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
|
state_group: filters?.state_group ? filters?.state_group.join(",") : undefined,
|
||||||
|
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
|
||||||
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
|
||||||
type: filters?.type ? filters?.type : undefined,
|
type: filters?.type ? filters?.type : undefined,
|
||||||
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
|
subscriber: filters?.subscriber ? filters?.subscriber.join(",") : undefined,
|
||||||
|
@ -183,9 +183,6 @@ const GeneralSettings: NextPage = () => {
|
|||||||
data={projectDetails ?? null}
|
data={projectDetails ?? null}
|
||||||
isOpen={Boolean(selectProject)}
|
isOpen={Boolean(selectProject)}
|
||||||
onClose={() => setSelectedProject(null)}
|
onClose={() => setSelectedProject(null)}
|
||||||
onSuccess={() => {
|
|
||||||
router.push(`/${workspaceSlug}/projects`);
|
|
||||||
}}
|
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-8">
|
||||||
|
@ -108,6 +108,8 @@ const MembersSettings: NextPage = () => {
|
|||||||
|
|
||||||
const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
|
const currentUser = projectMembers?.find((item) => item.member.id === user?.id);
|
||||||
|
|
||||||
|
const handleProjectInvitationSuccess = () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectAuthorizationWrapper
|
<ProjectAuthorizationWrapper
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
@ -167,6 +169,7 @@ const MembersSettings: NextPage = () => {
|
|||||||
setIsOpen={setInviteModal}
|
setIsOpen={setInviteModal}
|
||||||
members={members}
|
members={members}
|
||||||
user={user}
|
user={user}
|
||||||
|
onSuccess={() => mutateMembers()}
|
||||||
/>
|
/>
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
|
@ -69,19 +69,6 @@ const MembersSettings: NextPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const members = [
|
const members = [
|
||||||
...(workspaceMembers?.map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
memberId: item.member?.id,
|
|
||||||
avatar: item.member?.avatar,
|
|
||||||
first_name: item.member?.first_name,
|
|
||||||
last_name: item.member?.last_name,
|
|
||||||
email: item.member?.email,
|
|
||||||
display_name: item.member?.display_name,
|
|
||||||
role: item.role,
|
|
||||||
status: true,
|
|
||||||
member: true,
|
|
||||||
accountCreated: true,
|
|
||||||
})) || []),
|
|
||||||
...(workspaceInvitations?.map((item) => ({
|
...(workspaceInvitations?.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
memberId: item.id,
|
memberId: item.id,
|
||||||
@ -95,10 +82,27 @@ const MembersSettings: NextPage = () => {
|
|||||||
member: false,
|
member: false,
|
||||||
accountCreated: item?.accepted ? false : true,
|
accountCreated: item?.accepted ? false : true,
|
||||||
})) || []),
|
})) || []),
|
||||||
|
...(workspaceMembers?.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
memberId: item.member?.id,
|
||||||
|
avatar: item.member?.avatar,
|
||||||
|
first_name: item.member?.first_name,
|
||||||
|
last_name: item.member?.last_name,
|
||||||
|
email: item.member?.email,
|
||||||
|
display_name: item.member?.display_name,
|
||||||
|
role: item.role,
|
||||||
|
status: true,
|
||||||
|
member: true,
|
||||||
|
accountCreated: true,
|
||||||
|
})) || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
|
const currentUser = workspaceMembers?.find((item) => item.member?.id === user?.id);
|
||||||
|
|
||||||
|
const handleInviteModalSuccess = () => {
|
||||||
|
mutateInvitations();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceAuthorizationLayout
|
<WorkspaceAuthorizationLayout
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
@ -139,8 +143,9 @@ const MembersSettings: NextPage = () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
mutateMembers((prevData: any) =>
|
mutateMembers(
|
||||||
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
|
(prevData: any) =>
|
||||||
|
prevData?.filter((item: any) => item.id !== selectedRemoveMember)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -180,6 +185,7 @@ const MembersSettings: NextPage = () => {
|
|||||||
setIsOpen={setInviteModal}
|
setIsOpen={setInviteModal}
|
||||||
workspace_slug={workspaceSlug as string}
|
workspace_slug={workspaceSlug as string}
|
||||||
user={user}
|
user={user}
|
||||||
|
onSuccess={handleInviteModalSuccess}
|
||||||
/>
|
/>
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<SettingsHeader />
|
<SettingsHeader />
|
||||||
|
@ -58,6 +58,25 @@
|
|||||||
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
--color-border-300: 212, 212, 212; /* strong border- 1 */
|
||||||
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
--color-border-400: 185, 185, 185; /* strong border- 2 */
|
||||||
|
|
||||||
|
--color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06),
|
||||||
|
0px 1px 2px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.14);
|
||||||
|
--color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12),
|
||||||
|
0px 1px 8px -1px rgba(16, 24, 40, 0.1);
|
||||||
|
--color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02),
|
||||||
|
0px 1px 12px 0px rgba(0, 0, 0, 0.12);
|
||||||
|
--color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08),
|
||||||
|
0px 1px 12px 0px rgba(16, 24, 40, 0.04);
|
||||||
|
--color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12),
|
||||||
|
0px 1px 16px 0px rgba(16, 24, 40, 0.12);
|
||||||
|
--color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||||
|
0px 1px 24px 0px rgba(16, 24, 40, 0.12);
|
||||||
|
--color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16),
|
||||||
|
0px 0px 52px 0px rgba(16, 24, 40, 0.16);
|
||||||
|
--color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12),
|
||||||
|
0px 12px 24px 0px rgba(16, 24, 40, 0.12), 0px 1px 32px 0px rgba(16, 24, 40, 0.12);
|
||||||
|
--color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12),
|
||||||
|
0px 1px 48px 0px rgba(16, 24, 40, 0.12);
|
||||||
|
|
||||||
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
|
||||||
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
|
||||||
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */
|
||||||
@ -71,6 +90,16 @@
|
|||||||
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
|
--color-sidebar-border-200: var(--color-border-100); /* subtle sidebar border- 2 */
|
||||||
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
|
--color-sidebar-border-300: var(--color-border-100); /* strong sidebar border- 1 */
|
||||||
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
|
--color-sidebar-border-400: var(--color-border-100); /* strong sidebar border- 2 */
|
||||||
|
|
||||||
|
--color-sidebar-shadow-2xs: var(--color-shadow-2xs);
|
||||||
|
--color-sidebar-shadow-xs: var(--color-shadow-xs);
|
||||||
|
--color-sidebar-shadow-sm: var(--color-shadow-sm);
|
||||||
|
--color-sidebar-shadow-rg: var(--color-shadow-rg);
|
||||||
|
--color-sidebar-shadow-md: var(--color-shadow-md);
|
||||||
|
--color-sidebar-shadow-lg: var(--color-shadow-lg);
|
||||||
|
--color-sidebar-shadow-xl: var(--color-shadow-xl);
|
||||||
|
--color-sidebar-shadow-2xl: var(--color-shadow-2xl);
|
||||||
|
--color-sidebar-shadow-3xl: var(--color-shadow-3xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"],
|
[data-theme="light"],
|
||||||
@ -113,6 +142,16 @@
|
|||||||
--color-background-100: 7, 7, 7; /* primary bg */
|
--color-background-100: 7, 7, 7; /* primary bg */
|
||||||
--color-background-90: 11, 11, 11; /* secondary bg */
|
--color-background-90: 11, 11, 11; /* secondary bg */
|
||||||
--color-background-80: 23, 23, 23; /* tertiary bg */
|
--color-background-80: 23, 23, 23; /* tertiary bg */
|
||||||
|
|
||||||
|
--color-shadow-2xs: 0px 0px 1px 0px rgba(0, 0, 0, 0.15), 0px 1px 3px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
--color-shadow-xs: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
--color-shadow-sm: 0px 0px 4px 0px rgba(0, 0, 0, 0.2), 0px 2px 6px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
--color-shadow-rg: 0px 0px 6px 0px rgba(0, 0, 0, 0.2), 0px 4px 6px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
--color-shadow-md: 0px 2px 8px 0px rgba(0, 0, 0, 0.2), 0px 4px 8px 0px rgba(0, 0, 0, 0.5);
|
||||||
|
--color-shadow-lg: 0px 4px 12px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.55);
|
||||||
|
--color-shadow-xl: 0px 0px 14px 0px rgba(0, 0, 0, 0.25), 0px 6px 10px 0px rgba(0, 0, 0, 0.55);
|
||||||
|
--color-shadow-2xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.25), 0px 8px 12px 0px rgba(0, 0, 0, 0.6);
|
||||||
|
--color-shadow-3xl: 0px 4px 24px 0px rgba(0, 0, 0, 0.3), 0px 12px 40px 0px rgba(0, 0, 0, 0.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
|
@ -5,6 +5,26 @@ module.exports = {
|
|||||||
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
content: ["./pages/**/*.tsx", "./components/**/*.tsx", "./layouts/**/*.tsx", "./ui/**/*.tsx"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
boxShadow: {
|
||||||
|
"custom-shadow-2xs": "var(--color-shadow-2xs)",
|
||||||
|
"custom-shadow-xs": "var(--color-shadow-xs)",
|
||||||
|
"custom-shadow-sm": "var(--color-shadow-sm)",
|
||||||
|
"custom-shadow-rg": "var(--color-shadow-rg)",
|
||||||
|
"custom-shadow-md": "var(--color-shadow-md)",
|
||||||
|
"custom-shadow-lg": "var(--color-shadow-lg)",
|
||||||
|
"custom-shadow-xl": "var(--color-shadow-xl)",
|
||||||
|
"custom-shadow-2xl": "var(--color-shadow-2xl)",
|
||||||
|
"custom-shadow-3xl": "var(--color-shadow-3xl)",
|
||||||
|
"custom-sidebar-shadow-2xs": "var(--color-sidebar-shadow-2xs)",
|
||||||
|
"custom-sidebar-shadow-xs": "var(--color-sidebar-shadow-xs)",
|
||||||
|
"custom-sidebar-shadow-sm": "var(--color-sidebar-shadow-sm)",
|
||||||
|
"custom-sidebar-shadow-rg": "var(--color-sidebar-shadow-rg)",
|
||||||
|
"custom-sidebar-shadow-md": "var(--color-sidebar-shadow-md)",
|
||||||
|
"custom-sidebar-shadow-lg": "var(--color-sidebar-shadow-lg)",
|
||||||
|
"custom-sidebar-shadow-xl": "var(--color-sidebar-shadow-xl)",
|
||||||
|
"custom-sidebar-shadow-2xl": "var(--color-sidebar-shadow-2xl)",
|
||||||
|
"custom-sidebar-shadow-3xl": "var(--color-sidebar-shadow-3xl)",
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
custom: {
|
custom: {
|
||||||
primary: {
|
primary: {
|
||||||
@ -182,8 +202,5 @@ module.exports = {
|
|||||||
custom: ["Inter", "sans-serif"],
|
custom: ["Inter", "sans-serif"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||||
require("tailwindcss-animate"),
|
|
||||||
require("@tailwindcss/typography")
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -215,6 +215,7 @@ export interface IIssueLite {
|
|||||||
export interface IIssueFilterOptions {
|
export interface IIssueFilterOptions {
|
||||||
type: "active" | "backlog" | null;
|
type: "active" | "backlog" | null;
|
||||||
assignees: string[] | null;
|
assignees: string[] | null;
|
||||||
|
start_date: string[] | null;
|
||||||
target_date: string[] | null;
|
target_date: string[] | null;
|
||||||
state: string[] | null;
|
state: string[] | null;
|
||||||
state_group: TStateGroups[] | null;
|
state_group: TStateGroups[] | null;
|
||||||
@ -233,6 +234,7 @@ export type TIssueGroupByOptions =
|
|||||||
| "created_by"
|
| "created_by"
|
||||||
| "state_detail.group"
|
| "state_detail.group"
|
||||||
| "project"
|
| "project"
|
||||||
|
| "assignees"
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
export type TIssueOrderByOptions =
|
export type TIssueOrderByOptions =
|
||||||
|
1
apps/app/types/views.d.ts
vendored
1
apps/app/types/views.d.ts
vendored
@ -20,6 +20,7 @@ export interface IQuery {
|
|||||||
labels: string[] | null;
|
labels: string[] | null;
|
||||||
priority: string[] | null;
|
priority: string[] | null;
|
||||||
state: string[] | null;
|
state: string[] | null;
|
||||||
|
start_date: string[] | null;
|
||||||
target_date: string[] | null;
|
target_date: string[] | null;
|
||||||
type: "active" | "backlog" | null;
|
type: "active" | "backlog" | null;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user