fix: merge conflict

This commit is contained in:
dakshesh14 2023-11-06 21:21:17 +05:30
commit a434b1008d
116 changed files with 1364 additions and 4242 deletions

205
.github/workflows/build-branch.yml vendored Normal file
View File

@ -0,0 +1,205 @@
name: Docker Branch Build
on:
workflow_dispatch:
inputs:
logLevel:
description: 'Log level'
required: true
default: 'warning'
tags:
description: 'Dev/QA Builds'
env:
gh_branch: ${{ github.ref_name }}
img_tag: latest
jobs:
branch_build_and_push:
name: Build-Push Web/Space/API/Proxy Docker Image
runs-on: ubuntu-20.04
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- uses: ASzc/change-string-case-action@v2
id: gh_branch_upper_lower
with:
string: ${{ env.gh_branch }}
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_slash
with:
source: ${{ steps.gh_branch_upper_lower.outputs.lowercase }}
find: '/'
replace: '-'
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_replace_dot
with:
source: ${{ steps.gh_branch_replace_slash.outputs.value }}
find: '.'
replace: ''
- uses: mad9000/actions-find-and-replace-string@2
id: gh_branch_clean
with:
source: ${{ steps.gh_branch_replace_dot.outputs.value }}
find: '_'
replace: ''
- name: Uploading Proxy Source
uses: actions/upload-artifact@v3
with:
name: proxy-src-code
path: ./nginx
- name: Uploading Backend Source
uses: actions/upload-artifact@v3
with:
name: backend-src-code
path: ./apiserver
- name: Uploading Web Source
uses: actions/upload-artifact@v3
with:
name: web-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./space
- name: Uploading Space Source
uses: actions/upload-artifact@v3
with:
name: space-src-code
path: |
./
!./apiserver
!./nginx
!./deploy
!./web
outputs:
gh_branch_name: ${{ steps.gh_branch_clean.outputs.value }}
branch_build_push_frontend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- 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: Downloading Web Source Code
uses: actions/download-artifact@v3
with:
name: web-src-code
- name: Build and Push Frontend to Docker Container Registry
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./web/Dockerfile.web
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_space:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- 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: Downloading Space Source Code
uses: actions/download-artifact@v3
with:
name: space-src-code
- name: Build and Push Space to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./space/Dockerfile.space
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-space-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_backend:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- 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: Downloading Backend Source Code
uses: actions/download-artifact@v3
with:
name: backend-src-code
- name: Build and Push Backend to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile.api
platforms: linux/amd64
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_proxy:
runs-on: ubuntu-20.04
needs: [ branch_build_and_push ]
steps:
- 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: Downloading Proxy Source Code
uses: actions/download-artifact@v3
with:
name: proxy-src-code
- name: Build and Push Plane-Proxy to Docker Hub
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy-private:${{ needs.branch_build_and_push.outputs.gh_branch_name }}
push: true
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}

2
.gitignore vendored
View File

@ -75,7 +75,7 @@ pnpm-lock.yaml
pnpm-workspace.yaml
.npmrc
.secrets
tmp/
## packages
dist

View File

@ -1,7 +1,7 @@
from .analytic import urlpatterns as analytic_urls
from .asset import urlpatterns as asset_urls
from .authentication import urlpatterns as authentication_urls
from .configuration import urlpatterns as configuration_urls
from .config import urlpatterns as configuration_urls
from .cycle import urlpatterns as cycle_urls
from .estimate import urlpatterns as estimate_urls
from .gpt import urlpatterns as gpt_urls

View File

@ -30,4 +30,5 @@ class ConfigurationEndpoint(BaseAPIView):
data["email_password_login"] = (
os.environ.get("ENABLE_EMAIL_PASSWORD", "0") == "1"
)
data["slack"] = os.environ.get("SLACK_CLIENT_ID", None)
return Response(data, status=status.HTTP_200_OK)

View File

@ -1,6 +1,6 @@
# Python improts
import uuid
import requests
# Django imports
from django.contrib.auth.hashers import make_password
@ -25,7 +25,7 @@ from plane.utils.integrations.github import (
delete_github_installation,
)
from plane.api.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
serializer_class = IntegrationSerializer
@ -98,12 +98,19 @@ class WorkspaceIntegrationViewSet(BaseViewSet):
config = {"installation_id": installation_id}
if provider == "slack":
metadata = request.data.get("metadata", {})
code = request.data.get("code", False)
if not code:
return Response({"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST)
slack_response = slack_oauth(code=code)
metadata = slack_response
access_token = metadata.get("access_token", False)
team_id = metadata.get("team", {}).get("id", False)
if not metadata or not access_token or not team_id:
return Response(
{"error": "Access token and team id is required"},
{"error": "Slack could not be installed. Please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
config = {"team_id": team_id, "access_token": access_token}

View File

@ -11,6 +11,7 @@ from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.utils.integrations.slack import slack_oauth
class SlackProjectSyncViewSet(BaseViewSet):
@ -32,25 +33,46 @@ class SlackProjectSyncViewSet(BaseViewSet):
)
def create(self, request, slug, project_id, workspace_integration_id):
serializer = SlackProjectSyncSerializer(data=request.data)
try:
code = request.data.get("code", False)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
if not code:
return Response(
{"error": "Code is required"}, status=status.HTTP_400_BAD_REQUEST
)
if serializer.is_valid():
serializer.save(
project_id=project_id,
workspace_integration_id=workspace_integration_id,
slack_response = slack_oauth(code=code)
workspace_integration = WorkspaceIntegration.objects.get(
workspace__slug=slug, pk=workspace_integration_id
)
workspace_integration = WorkspaceIntegration.objects.get(
pk=workspace_integration_id, workspace__slug=slug
)
slack_project_sync = SlackProjectSync.objects.create(
access_token=slack_response.get("access_token"),
scopes=slack_response.get("scope"),
bot_user_id=slack_response.get("bot_user_id"),
webhook_url=slack_response.get("incoming_webhook", {}).get("url"),
data=slack_response,
team_id=slack_response.get("team", {}).get("id"),
team_name=slack_response.get("team", {}).get("name"),
workspace_integration=workspace_integration,
)
_ = ProjectMember.objects.get_or_create(
member=workspace_integration.actor, role=20, project_id=project_id
)
serializer = SlackProjectSyncSerializer(slack_project_sync)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"error": "Slack is already installed for the project"},
status=status.HTTP_410_GONE,
)
capture_exception(e)
return Response(
{"error": "Slack could not be installed. Please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)

View File

@ -0,0 +1,20 @@
import os
import requests
def slack_oauth(code):
SLACK_OAUTH_URL = os.environ.get("SLACK_OAUTH_URL", False)
SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID", False)
SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET", False)
# Oauth Slack
if SLACK_OAUTH_URL and SLACK_CLIENT_ID and SLACK_CLIENT_SECRET:
response = requests.get(
SLACK_OAUTH_URL,
params={
"code": code,
"client_id": SLACK_CLIENT_ID,
"client_secret": SLACK_CLIENT_SECRET,
},
)
return response.json()
return {}

View File

@ -2,6 +2,7 @@
"name": "@plane/editor-core",
"version": "0.0.1",
"description": "Core Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",

View File

@ -2,6 +2,7 @@
"name": "@plane/lite-text-editor",
"version": "0.0.1",
"description": "Package that powers Plane's Comment Editor",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@ -29,9 +30,6 @@
"dependencies": {
"@plane/editor-core": "*",
"@tiptap/extension-list-item": "^2.1.11",
"@types/node": "18.15.3",
"@types/react": "^18.2.5",
"@types/react-dom": "18.0.11",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"eslint": "8.36.0",
@ -46,6 +44,9 @@
"use-debounce": "^9.0.4"
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.35",
"@types/react-dom": "^18.2.14",
"eslint": "^7.32.0",
"postcss": "^8.4.29",
"tailwind-config-custom": "*",

View File

@ -1,4 +1,3 @@
"use client";
import * as React from "react";
import {
EditorContainer,
@ -32,7 +31,7 @@ interface ILiteTextEditor {
editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void;
setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
isSubmitting: "submitting" | "submitted" | "saved"
) => void;
setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any;
@ -127,7 +126,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
};
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />
);
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";

View File

@ -1,6 +1,10 @@
"use client"
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core';
import * as React from 'react';
import * as React from "react";
import {
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useReadOnlyEditor,
} from "@plane/editor-core";
interface ICoreReadOnlyEditor {
value: string;
@ -8,7 +12,7 @@ interface ICoreReadOnlyEditor {
noBorder?: boolean;
borderOnFocus?: boolean;
customClassName?: string;
mentionHighlights: string[]
mentionHighlights: string[];
}
interface EditorCoreProps extends ICoreReadOnlyEditor {
@ -27,31 +31,39 @@ const LiteReadOnlyEditor = ({
customClassName,
value,
forwardedRef,
mentionHighlights
mentionHighlights,
}: EditorCoreProps) => {
const editor = useReadOnlyEditor({
value,
forwardedRef,
mentionHighlights
mentionHighlights,
});
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
const editorClassNames = getEditorClassNames({
noBorder,
borderOnFocus,
customClassName,
});
if (!editor) return null;
return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col">
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
<EditorContentWrapper
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div>
</EditorContainer >
</EditorContainer>
);
};
const LiteReadOnlyEditorWithRef = React.forwardRef<EditorHandle, ICoreReadOnlyEditor>((props, ref) => (
<LiteReadOnlyEditor {...props} forwardedRef={ref} />
));
const LiteReadOnlyEditorWithRef = React.forwardRef<
EditorHandle,
ICoreReadOnlyEditor
>((props, ref) => <LiteReadOnlyEditor {...props} forwardedRef={ref} />);
LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
export { LiteReadOnlyEditor , LiteReadOnlyEditorWithRef };
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef };

View File

@ -1,5 +1,4 @@
import * as React from 'react';
import * as React from "react";
// next-themes
import { useTheme } from "next-themes";
// tooltip2
@ -69,8 +68,16 @@ export const Tooltip: React.FC<Props> = ({
</div>
}
position={position}
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props })
renderTarget={({
isOpen: isTooltipOpen,
ref: eleReference,
...tooltipProps
}) =>
React.cloneElement(children, {
ref: eleReference,
...tooltipProps,
...children.props,
})
}
/>
);

View File

@ -2,6 +2,7 @@
"name": "@plane/rich-text-editor",
"version": "0.0.1",
"description": "Rich Text Editor that powers Plane",
"private": true,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@ -21,19 +22,19 @@
"check-types": "tsc --noEmit"
},
"peerDependencies": {
"@tiptap/core": "^2.1.11",
"next": "12.3.2",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "18.2.0",
"@tiptap/core": "^2.1.11"
"react-dom": "18.2.0"
},
"dependencies": {
"@plane/editor-core": "*",
"@tiptap/extension-code-block-lowlight": "^2.1.11",
"@tiptap/extension-horizontal-rule": "^2.1.11",
"@tiptap/extension-placeholder": "^2.1.11",
"class-variance-authority": "^0.7.0",
"@tiptap/suggestion": "^2.1.7",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"highlight.js": "^11.8.0",
"lowlight": "^3.0.0",
@ -41,8 +42,8 @@
},
"devDependencies": {
"@types/node": "18.15.3",
"@types/react": "^18.2.5",
"@types/react-dom": "18.0.11",
"@types/react": "^18.2.35",
"@types/react-dom": "^18.2.14",
"eslint": "^7.32.0",
"postcss": "^8.4.29",
"react": "^18.2.0",

View File

@ -8,6 +8,7 @@ interface IRichTextReadOnlyEditor {
noBorder?: boolean;
borderOnFocus?: boolean;
customClassName?: string;
mentionHighlights?: string[];
}
interface RichTextReadOnlyEditorProps extends IRichTextReadOnlyEditor {
@ -26,10 +27,12 @@ const RichReadOnlyEditor = ({
customClassName,
value,
forwardedRef,
mentionHighlights,
}: RichTextReadOnlyEditorProps) => {
const editor = useReadOnlyEditor({
value,
forwardedRef,
mentionHighlights,
});
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });

View File

@ -1,5 +1,6 @@
{
"name": "eslint-config-custom",
"private": true,
"version": "0.13.2",
"main": "index.js",
"license": "MIT",

View File

@ -3,6 +3,7 @@
"version": "0.13.2",
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"private": true,
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",

View File

@ -1,5 +1,7 @@
{
"name": "@plane/ui",
"description": "UI components shared across multiple apps internally",
"private": true,
"version": "0.0.1",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View File

@ -35,8 +35,11 @@ export const AvatarGroup: React.FC<Props> = (props) => {
// calculate total length of avatars inside the group
const totalAvatars = React.Children.toArray(children).length;
// if avatars are equal to max + 1, then we need to show the last avatar as well, if avatars are more than max + 1, then we need to show the count of the remaining avatars
const maxAvatarsToRender = totalAvatars <= max + 1 ? max + 1 : max;
// slice the children to the maximum number of avatars
const avatars = React.Children.toArray(children).slice(0, max);
const avatars = React.Children.toArray(children).slice(0, maxAvatarsToRender);
// assign the necessary props from the AvatarGroup component to the Avatar components
const avatarsWithUpdatedProps = avatars.map((avatar) => {
@ -54,11 +57,14 @@ export const AvatarGroup: React.FC<Props> = (props) => {
return (
<div className={`flex ${sizeInfo.spacing}`}>
{avatarsWithUpdatedProps.map((avatar, index) => (
<div key={index} className="ring-1 ring-custom-border-200 rounded-full">
<div
key={index}
className="ring-1 ring-custom-background-100 rounded-full"
>
{avatar}
</div>
))}
{max < totalAvatars && (
{maxAvatarsToRender < totalAvatars && (
<Tooltip
tooltipContent={`${totalAvatars} total`}
disabled={!showTooltip}
@ -66,7 +72,7 @@ export const AvatarGroup: React.FC<Props> = (props) => {
<div
className={`${
!isAValidNumber(size) ? sizeInfo.avatarSize : ""
} ring-1 ring-custom-border-200 bg-custom-primary-500 text-white rounded-full grid place-items-center text-[9px]`}
} ring-1 ring-custom-background-100 bg-custom-primary-10 text-custom-primary-100 rounded-full grid place-items-center text-[9px]`}
style={
isAValidNumber(size)
? {

View File

@ -1 +0,0 @@
export * from "./state-group";

View File

@ -1,23 +0,0 @@
import React from "react";
// types
import type { Props } from "../types";
// constants
import { issueGroupColors } from "constants/data";
export const BacklogStateIcon: React.FC<Props> = ({
width = "14",
height = "14",
className,
color = issueGroupColors["backlog"],
}) => (
<svg
width={width}
height={height}
className={className}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="10" cy="10" r="9" stroke={color} strokeLinecap="round" strokeDasharray="4 4" />
</svg>
);

View File

@ -1,74 +0,0 @@
import React from "react";
// types
import type { Props } from "../types";
// constants
import { issueGroupColors } from "constants/data";
export const CancelledStateIcon: React.FC<Props> = ({
width = "14",
height = "14",
className,
color = issueGroupColors["cancelled"],
}) => (
<svg width={width} height={height} className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.36 84.36">
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,32.44q9.54,9.75,19.09,19.48"
/>
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M32.64,51.92,51.73,32.44"
/>
</g>
</g>
</svg>
);

View File

@ -1,65 +0,0 @@
import React from "react";
// types
import type { Props } from "../types";
// constants
import { issueGroupColors } from "constants/data";
export const CompletedStateIcon: React.FC<Props> = ({
width = "14",
height = "14",
className,
color = issueGroupColors["completed"],
}) => (
<svg width={width} height={height} className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.36 84.36">
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
strokeWidth={3}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
<circle className="cls-2" fill={color} cx="42.18" cy="42.18" r="31.04" />
<path
className="cls-3"
fill="none"
strokeWidth={3}
stroke="#ffffff"
strokeLinecap="square"
strokeMiterlimit={10}
d="M30.45,43.75l6.61,6.61L53.92,34"
/>
</g>
</g>
</svg>
);

View File

@ -1,6 +0,0 @@
export * from "./backlog-state-icon";
export * from "./cancelled-state-icon";
export * from "./completed-state-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./unstarted-state-icon";

View File

@ -1,73 +0,0 @@
import React from "react";
// types
import type { Props } from "../types";
// constants
import { issueGroupColors } from "constants/data";
export const StartedStateIcon: React.FC<Props> = ({
width = "14",
height = "14",
className,
color = issueGroupColors["started"],
}) => (
<svg width={width} height={height} className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 83.36 83.36">
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20,7.19a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.17,20a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.42,76.17A39.78,39.78,0,0,1,20,75.64"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.19,63.42A39.75,39.75,0,0,1,7.73,20"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.21q9.57-14.45,19.13-28.9a35.8,35.8,0,0,0-39.09,0Z"
/>
<path
className="cls-2"
fill={color}
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M42.32,41.7,61.45,70.6a35.75,35.75,0,0,1-39.09,0Z"
/>
</g>
</g>
</svg>
);

View File

@ -1,29 +0,0 @@
// icons
import {
BacklogStateIcon,
CancelledStateIcon,
CompletedStateIcon,
StartedStateIcon,
UnstartedStateIcon,
} from "components/icons";
import { TIssueGroupKey } from "types/issue";
type Props = {
stateGroup: TIssueGroupKey;
color: string;
className?: string;
height?: string;
width?: string;
};
export const StateGroupIcon: React.FC<Props> = ({ stateGroup, className, color, height = "12px", width = "12px" }) => {
if (stateGroup === "backlog")
return <BacklogStateIcon className={className} color={color} height={height} width={width} />;
else if (stateGroup === "cancelled")
return <CancelledStateIcon className={className} color={color} height={height} width={width} />;
else if (stateGroup === "completed")
return <CompletedStateIcon className={className} color={color} height={height} width={width} />;
else if (stateGroup === "started")
return <StartedStateIcon className={className} color={color} height={height} width={width} />;
else return <UnstartedStateIcon className={className} color={color} height={height} width={width} />;
};

View File

@ -1,55 +0,0 @@
import React from "react";
// types
import type { Props } from "../types";
// constants
import { issueGroupColors } from "constants/data";
export const UnstartedStateIcon: React.FC<Props> = ({
width = "14",
height = "14",
className,
color = issueGroupColors["unstarted"],
}) => (
<svg width={width} height={height} className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.36 84.36">
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M20.45,7.69a39.74,39.74,0,0,1,43.43.54"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M76.67,20.45a39.76,39.76,0,0,1-.53,43.43"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M63.92,76.67a39.78,39.78,0,0,1-43.44-.53"
/>
<path
className="cls-1"
fill="none"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M7.69,63.92a39.75,39.75,0,0,1,.54-43.44"
/>
</g>
</g>
</svg>
);

View File

@ -1,6 +0,0 @@
export type Props = {
width?: string | number;
height?: string | number;
color?: string;
className?: string;
};

View File

@ -1,3 +1,5 @@
// ui
import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "constants/data";
@ -8,7 +10,7 @@ export const IssueBlockState = ({ state }: any) => {
return (
<div className="flex items-center justify-between gap-1 w-full rounded shadow-sm border-[0.5px] border-custom-border-300 duration-300 focus:outline-none px-2.5 py-1 text-xs">
<div className="flex items-center w-full gap-1.5 text-custom-text-200">
<stateGroup.icon />
<StateGroupIcon stateGroup={state.group} color={state.color} />
<div className="text-xs">{state?.name}</div>
</div>
</div>

View File

@ -7,7 +7,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components
import { IssueBlockPriority } from "components/issues/board-views/block-priority";
import { IssueBlockState } from "components/issues/board-views/block-state";
import { IssueBlockLabels } from "components/issues/board-views/block-labels";
import { IssueBlockDueDate } from "components/issues/board-views/block-due-date";
// interfaces
import { IIssue } from "types/issue";
@ -37,7 +36,7 @@ export const IssueListBlock = observer(({ issue }: { issue: IIssue }) => {
};
return (
<div className="py-3 px-4 h-[118px] flex flex-col gap-1.5 bg-custom-background-100 rounded shadow-custom-shadow-sm border-[0.5px] border-custom-border-200">
<div className="py-2 px-3 flex flex-col gap-1.5 bg-custom-background-100 rounded shadow-custom-shadow-2xs border-[0.5px] border-custom-border-200 space-y-2 text-sm">
{/* id */}
<div className="text-xs text-custom-text-300 break-words">
{projectStore?.project?.identifier}-{issue?.sequence_id}

View File

@ -4,8 +4,8 @@ import { observer } from "mobx-react-lite";
import { IIssueState } from "types/issue";
// constants
import { issueGroupFilter } from "constants/data";
// icons
import { StateGroupIcon } from "components/icons";
// ui
import { StateGroupIcon } from "@plane/ui";
// mobx hook
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
@ -18,11 +18,11 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
if (stateGroup === null) return <></>;
return (
<div className="pb-2 px-2 flex items-center">
<div className="w-4 h-4 flex justify-center items-center flex-shrink-0">
<StateGroupIcon stateGroup={state.group} color={state.color} />
<div className="pb-2 px-2 flex items-center gap-2">
<div className="w-3.5 h-3.5 flex justify-center items-center flex-shrink-0">
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
</div>
<div className="font-semibold text-custom-text-200 capitalize ml-2 mr-3 truncate">{state?.name}</div>
<div className="font-semibold text-custom-text-200 capitalize mr-1 truncate">{state?.name}</div>
<span className="text-custom-text-300 rounded-full flex-shrink-0">
{store.issue.getCountOfIssuesByState(state.id)}
</span>

View File

@ -38,10 +38,10 @@ export const IssueListBlock: FC<{ issue: IIssue }> = observer((props) => {
};
return (
<div className="flex items-center px-6 py-3.5 relative gap-10 bg-custom-background-100">
<div className="relative flex items-center gap-5 w-full flex-grow overflow-hidden">
<div className="flex items-center p-3 relative gap-10 bg-custom-background-100">
<div className="relative flex items-center gap-3 w-full flex-grow overflow-hidden">
{/* id */}
<div className="flex-shrink-0 text-sm text-custom-text-300">
<div className="flex-shrink-0 text-xs text-custom-text-300 font-medium">
{projectStore?.project?.identifier}-{issue?.sequence_id}
</div>
{/* name */}

View File

@ -2,8 +2,8 @@
import { observer } from "mobx-react-lite";
// interfaces
import { IIssueState } from "types/issue";
// icons
import { StateGroupIcon } from "components/icons";
// ui
import { StateGroupIcon } from "@plane/ui";
// constants
import { issueGroupFilter } from "constants/data";
// mobx hook
@ -18,12 +18,12 @@ export const IssueListHeader = observer(({ state }: { state: IIssueState }) => {
if (stateGroup === null) return <></>;
return (
<div className="px-6 py-2 flex items-center">
<div className="w-4 h-4 flex justify-center items-center">
<StateGroupIcon stateGroup={state.group} color={state.color} />
<div className="p-3 flex items-center gap-2">
<div className="w-3.5 h-3.5 flex justify-center items-center">
<StateGroupIcon stateGroup={state.group} color={state.color} height="14" width="14" />
</div>
<div className="font-semibold capitalize ml-2 mr-3">{state?.name}</div>
<div className="text-custom-text-200">{store.issue.getCountOfIssuesByState(state.id)}</div>
<div className="font-medium capitalize mr-1">{state?.name}</div>
<div className="text-custom-text-200 text-sm font-medium">{store.issue.getCountOfIssuesByState(state.id)}</div>
</div>
);
});

View File

@ -27,9 +27,7 @@ export const IssueListView = observer(() => {
))}
</div>
) : (
<div className="px-6 py-3.5 text-sm text-custom-text-200 bg-custom-background-100">
No Issues are available.
</div>
<div className="p-3 text-sm text-custom-text-400 bg-custom-background-100">No issues.</div>
)}
</div>
))}

View File

@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// ui
import { SecondaryButton } from "components/ui";
import { Button } from "@plane/ui";
// types
import { Comment } from "types/issue";
// components
@ -29,7 +29,6 @@ export const AddComment: React.FC<Props> = observer((props) => {
const {
handleSubmit,
control,
setValue,
watch,
formState: { isSubmitting },
reset,
@ -85,27 +84,30 @@ export const AddComment: React.FC<Props> = observer((props) => {
? watch("comment_html")
: value
}
customClassName="p-3 min-h-[50px] shadow-sm"
customClassName="p-2"
editorContentCustomClassNames="min-h-[35px]"
debouncedUpdatesEnabled={false}
onChange={(comment_json: Object, comment_html: string) => {
onChange(comment_html);
}}
submitButton={
<Button
disabled={isSubmitting || disabled}
variant="primary"
type="submit"
className="!px-2.5 !py-1.5 !text-xs"
onClick={(e) => {
userStore.requiredLogin(() => {
handleSubmit(onSubmit)(e);
});
}}
>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
}
/>
)}
/>
<SecondaryButton
onClick={(e) => {
userStore.requiredLogin(() => {
handleSubmit(onSubmit)(e);
});
}}
type="submit"
disabled={isSubmitting || disabled}
className="mt-2"
>
{isSubmitting ? "Adding..." : "Comment"}
</SecondaryButton>
</div>
</div>
);

View File

@ -15,6 +15,7 @@ import { timeAgo } from "helpers/date-time.helper";
import { Comment } from "types/issue";
// services
import fileService from "services/file.service";
import useEditorSuggestions from "hooks/use-editor-suggestions";
type Props = {
workspaceSlug: string;
@ -28,6 +29,8 @@ export const CommentCard: React.FC<Props> = observer((props) => {
// states
const [isEditing, setIsEditing] = useState(false);
const mentionsConfig = useEditorSuggestions();
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
@ -135,6 +138,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
ref={showEditorRef}
value={comment.comment_html}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
mentionHighlights={mentionsConfig.mentionHighlights}
/>
<CommentReactions commentId={comment.id} projectId={comment.project} />
</div>

View File

@ -2,27 +2,33 @@ import { IssueReactions } from "components/issues/peek-overview";
import { RichReadOnlyEditor } from "@plane/rich-text-editor";
// types
import { IIssue } from "types/issue";
import useEditorSuggestions from "hooks/use-editor-suggestions";
type Props = {
issueDetails: IIssue;
};
export const PeekOverviewIssueDetails: React.FC<Props> = ({ issueDetails }) => (
<div className="space-y-2">
<h6 className="font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6>
<h4 className="break-words text-2xl font-semibold">{issueDetails.name}</h4>
{issueDetails.description_html !== "" && issueDetails.description_html !== "<p></p>" && (
<RichReadOnlyEditor
value={!issueDetails.description_html ||
issueDetails.description_html === "" ||
(typeof issueDetails.description_html === "object" &&
Object.keys(issueDetails.description_html).length === 0)
? "<p></p>"
: issueDetails.description_html}
customClassName="p-3 min-h-[50px] shadow-sm" />
)}
<IssueReactions />
</div>
);
export const PeekOverviewIssueDetails: React.FC<Props> = ({ issueDetails }) => {
const mentionConfig = useEditorSuggestions();
return (
<div className="space-y-2">
<h6 className="font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6>
<h4 className="break-words text-2xl font-semibold">{issueDetails.name}</h4>
{issueDetails.description_html !== "" && issueDetails.description_html !== "<p></p>" && (
<RichReadOnlyEditor
value={!issueDetails.description_html ||
issueDetails.description_html === "" ||
(typeof issueDetails.description_html === "object" &&
Object.keys(issueDetails.description_html).length === 0)
? "<p></p>"
: issueDetails.description_html}
customClassName="p-3 min-h-[50px] shadow-sm" mentionHighlights={mentionConfig.mentionHighlights} />
)}
<IssueReactions />
</div>
)
};

View File

@ -0,0 +1,13 @@
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
const useEditorSuggestions = () => {
const { mentionsStore }: RootStore = useMobxStore();
return {
// mentionSuggestions: mentionsStore.mentionSuggestions,
mentionHighlights: mentionsStore.mentionHighlights,
};
};
export default useEditorSuggestions;

View File

@ -0,0 +1,45 @@
import { IMentionHighlight } from "@plane/lite-text-editor";
import { RootStore } from "./root";
import { computed, makeObservable } from "mobx";
export interface IMentionsStore {
// mentionSuggestions: IMentionSuggestion[];
mentionHighlights: IMentionHighlight[];
}
export class MentionsStore implements IMentionsStore{
// root store
rootStore;
constructor(_rootStore: RootStore ){
// rootStore
this.rootStore = _rootStore;
makeObservable(this, {
mentionHighlights: computed,
// mentionSuggestions: computed
})
}
// get mentionSuggestions() {
// const projectMembers = this.rootStore.project.project.
// const suggestions = projectMembers === null ? [] : projectMembers.map((member) => ({
// id: member.member.id,
// type: "User",
// title: member.member.display_name,
// subtitle: member.member.email ?? "",
// avatar: member.member.avatar,
// redirect_uri: `/${member.workspace.slug}/profile/${member.member.id}`,
// }))
// return suggestions
// }
get mentionHighlights() {
const user = this.rootStore.user.currentUser;
return user ? [user.id] : []
}
}

View File

@ -5,6 +5,7 @@ import UserStore from "./user";
import IssueStore, { IIssueStore } from "./issue";
import ProjectStore, { IProjectStore } from "./project";
import IssueDetailStore, { IIssueDetailStore } from "./issue_details";
import { IMentionsStore, MentionsStore } from "./mentions.store";
enableStaticRendering(typeof window === "undefined");
@ -13,11 +14,13 @@ export class RootStore {
issue: IIssueStore;
issueDetails: IIssueDetailStore;
project: IProjectStore;
mentionsStore: IMentionsStore;
constructor() {
this.user = new UserStore(this);
this.issue = new IssueStore(this);
this.project = new ProjectStore(this);
this.issueDetails = new IssueDetailStore(this);
this.mentionsStore = new MentionsStore(this);
}
}

View File

@ -2,7 +2,6 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// service
import UserService from "services/user.service";
import { ActorDetail } from "types/issue";
// types
import { IUser } from "types/user";

View File

@ -199,9 +199,9 @@
--color-sidebar-text-400: var(--color-text-400); /* sidebar placeholder text */
--color-sidebar-border-100: var(--color-border-100); /* subtle sidebar border= 1 */
--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-400: var(--color-border-100); /* strong sidebar border- 2 */
--color-sidebar-border-200: var(--color-border-200); /* subtle sidebar border- 2 */
--color-sidebar-border-300: var(--color-border-300); /* strong sidebar border- 1 */
--color-sidebar-border-400: var(--color-border-400); /* strong sidebar border- 2 */
}
}

View File

@ -22,7 +22,8 @@
"SLACK_CLIENT_SECRET",
"JITSU_TRACKER_ACCESS_KEY",
"JITSU_TRACKER_HOST",
"UNSPLASH_ACCESS_KEY"
"UNSPLASH_ACCESS_KEY",
"NEXT_PUBLIC_SLACK_CLIENT_ID"
],
"pipeline": {
"build": {

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
// react beautiful dnd
import { Droppable, DroppableProps } from "react-beautiful-dnd";
import { Droppable, DroppableProps } from "@hello-pangea/dnd";
const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { MoreVertical } from "lucide-react";
// hooks

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { MoreVertical } from "lucide-react";
// hooks

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
import { DragDropContext, Draggable, DropResult } from "@hello-pangea/dnd";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { MoreVertical } from "lucide-react";
// hooks

View File

@ -50,9 +50,9 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
const editorRef = React.useRef<any>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined);
const editorSuggestions = useEditorSuggestions();
const {
control,
@ -88,7 +88,8 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
deleteFile={fileService.deleteImage}
ref={editorRef}
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
customClassName="p-3 min-h-[100px] shadow-sm"
customClassName="p-2 h-full"
editorContentCustomClassNames="min-h-[35px]"
debouncedUpdatesEnabled={false}
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
commentAccessSpecifier={
@ -98,15 +99,21 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
}
mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights}
submitButton={
<Button
variant="primary"
type="submit"
className="!px-2.5 !py-1.5 !text-xs"
disabled={isSubmitting || disabled}
>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
}
/>
)}
/>
)}
/>
<Button variant="neutral-primary" type="submit" disabled={isSubmitting || disabled}>
{isSubmitting ? "Adding..." : "Comment"}
</Button>
</div>
</form>
</div>

View File

@ -40,7 +40,7 @@ export const CommentCard: React.FC<Props> = ({
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const editorSuggestions = useEditorSuggestions(workspaceSlug, comment.project_detail.id)
const editorSuggestions = useEditorSuggestions();
const [isEditing, setIsEditing] = useState(false);

View File

@ -38,7 +38,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
const { setShowAlert } = useReloadConfirmations();
const editorSuggestion = useEditorSuggestions(workspaceSlug, issue.project_id)
const editorSuggestion = useEditorSuggestions();
const {
handleSubmit,
@ -164,8 +164,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
)}
/>
<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"}
</div>

View File

@ -119,7 +119,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const { setToastAlert } = useToast();
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId);
const editorSuggestions = useEditorSuggestions();
const {
formState: { errors, isSubmitting, isDirty },

View File

@ -112,7 +112,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
const user = userStore.currentUser;
const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId);
const editorSuggestion = useEditorSuggestions();
const { setToastAlert } = useToast();

View File

@ -1,38 +1,85 @@
import { useState } from "react";
import { observer } from "mobx-react-lite";
import { PlusIcon } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// components
import { EmptyState } from "components/common";
import { ExistingIssuesListModal } from "components/core";
// ui
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
import { Button } from "@plane/ui";
// types
import { ISearchIssueResponse } from "types";
type Props = {
openIssuesListModal: () => void;
workspaceSlug: string | undefined;
projectId: string | undefined;
cycleId: string | undefined;
};
export const CycleEmptyState: React.FC<Props> = ({ openIssuesListModal }) => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} onClick={openIssuesListModal} />}
>
Add an existing issue
</Button>
}
/>
</div>
);
export const CycleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, cycleId } = props;
// states
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const { cycleIssue: cycleIssueStore } = useMobxStore();
const { setToastAlert } = useToast();
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !cycleId) return;
const issueIds = data.map((i) => i.id);
await cycleIssueStore
.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
});
});
};
return (
<>
<ExistingIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
/>
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Cycle issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setCycleIssuesListModal(true)}
>
Add an existing issue
</Button>
}
/>
</div>
</>
);
});

View File

@ -4,36 +4,78 @@ import { EmptyState } from "components/common";
import { Button } from "@plane/ui";
// assets
import emptyIssue from "public/empty-state/issue.svg";
import { ExistingIssuesListModal } from "components/core";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
import { ISearchIssueResponse } from "types";
import useToast from "hooks/use-toast";
import { useState } from "react";
type Props = {
openIssuesListModal: () => void;
workspaceSlug: string | undefined;
projectId: string | undefined;
moduleId: string | undefined;
};
export const ModuleEmptyState: React.FC<Props> = ({ openIssuesListModal }) => (
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={openIssuesListModal}
>
Add an existing issue
</Button>
}
/>
</div>
);
export const ModuleEmptyState: React.FC<Props> = observer((props) => {
const { workspaceSlug, projectId, moduleId } = props;
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const { moduleIssue: moduleIssueStore } = useMobxStore();
const { setToastAlert } = useToast();
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;
const issueIds = data.map((i) => i.id);
await moduleIssueStore
.addIssueToModule(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), issueIds)
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the module. Please try again.",
})
);
};
return (
<>
<ExistingIssuesListModal
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: true }}
handleOnSubmit={handleAddIssuesToModule}
/>
<div className="h-full w-full grid place-items-center">
<EmptyState
title="Module issues will appear here"
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
image={emptyIssue}
primaryButton={{
text: "New issue",
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
onClick: () => {
const e = new KeyboardEvent("keydown", {
key: "c",
});
document.dispatchEvent(e);
},
}}
secondaryButton={
<Button
variant="neutral-primary"
prependIcon={<PlusIcon className="h-3 w-3" strokeWidth={2} />}
onClick={() => setModuleIssuesListModal(true)}
>
Add an existing issue
</Button>
}
/>
</div>
</>
);
});

View File

@ -42,7 +42,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
return (
<>
<Draggable draggableId={issue.id} index={index} isDragDisabled={isDragDisabled}>
<Draggable draggableId={issue.id} index={index}>
{(provided, snapshot) => (
<div
className="group/kanban-block relative p-1.5 hover:cursor-default"
@ -61,7 +61,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
)}
</div>
<div
className={`text-sm rounded p-2 px-3 shadow-custom-shadow-2xs space-y-[8px] border transition-all bg-custom-background-100 ${
className={`text-sm rounded py-2 px-3 shadow-custom-shadow-2xs space-y-2 border-[0.5px] border-custom-border-200 transition-all bg-custom-background-100 ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
>

View File

@ -10,11 +10,13 @@ import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/is
import { IIssueDisplayProperties, IIssue } from "types";
// constants
import { getValueFromObject } from "constants/issue";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
export interface IGroupByKanBan {
issues: any;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
sub_group_id: string;
list: any;
listKey: string;
@ -31,6 +33,7 @@ export interface IGroupByKanBan {
kanBanToggle: any;
handleKanBanToggle: any;
enableQuickIssueCreate?: boolean;
isDragStarted?: boolean;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -38,6 +41,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
issues,
sub_group_by,
group_by,
order_by,
sub_group_id = "null",
list,
listKey,
@ -49,17 +53,20 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
kanBanToggle,
handleKanBanToggle,
enableQuickIssueCreate,
isDragStarted,
} = props;
const verticalAlignPosition = (_list: any) =>
kanBanToggle?.groupByHeaderMinMax.includes(getValueFromObject(_list, listKey) as string);
return (
<div className="relative w-full h-full flex">
<div className="relative w-full h-full flex gap-3">
{list &&
list.length > 0 &&
list.map((_list: any) => (
<div className={`flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``}`}>
<div
className={`relative flex-shrink-0 flex flex-col ${!verticalAlignPosition(_list) ? `w-[340px]` : ``} group`}
>
{sub_group_by === null && (
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky top-0 z-[2]">
<KanBanGroupByHeaderRoot
@ -79,7 +86,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
verticalAlignPosition(_list) ? `w-[0px] overflow-hidden` : `w-full transition-all`
}`}
>
<Droppable droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}>
<Droppable
droppableId={`${getValueFromObject(_list, listKey) as string}__${sub_group_id}`}
isDropDisabled={isDragDisabled}
>
{(provided: any, snapshot: any) => (
<div
className={`w-full h-full relative transition-all ${
@ -101,25 +111,39 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
/>
) : (
isDragDisabled && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center text-sm">
{/* <div className="text-custom-text-300 text-sm">Drop here</div> */}
</div>
)
)}
{provided.placeholder}
</div>
)}
</Droppable>
</div>
{enableQuickIssueCreate && (
<BoardInlineCreateIssueForm
groupId={getValueFromObject(_list, listKey) as string}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
/>
<div className="flex-shrink-0 w-full bg-custom-background-90 py-1 sticky bottom-0 z-[0]">
{enableQuickIssueCreate && (
<BoardInlineCreateIssueForm
groupId={getValueFromObject(_list, listKey) as string}
subGroupId={sub_group_id}
prePopulatedData={{
...(group_by && { [group_by]: getValueFromObject(_list, listKey) }),
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
}}
/>
)}
</div>
{isDragStarted && isDragDisabled && (
<div className="invisible group-hover:visible transition-all text-sm absolute top-12 bottom-10 left-0 right-0 bg-custom-background-100/40 text-center">
<div className="rounded inline-flex mt-80 h-8 px-3 justify-center items-center bg-custom-background-80 text-custom-text-100 font-medium">
{`This board is ordered by "${replaceUnderscoreIfSnakeCase(
order_by ? (order_by[0] === "-" ? order_by.slice(1) : order_by) : "created_at"
)}"`}
</div>
</div>
)}
</div>
))}
@ -131,8 +155,8 @@ export interface IKanBan {
issues: any;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
sub_group_id?: string;
handleDragDrop?: (result: any) => void | undefined;
handleIssues: (
sub_group_by: string | null,
group_by: string | null,
@ -151,6 +175,7 @@ export interface IKanBan {
members: any;
projects: any;
enableQuickIssueCreate?: boolean;
isDragStarted?: boolean;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
@ -158,6 +183,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
issues,
sub_group_by,
group_by,
order_by,
sub_group_id = "null",
handleIssues,
quickActions,
@ -172,6 +198,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
members,
projects,
enableQuickIssueCreate,
isDragStarted,
} = props;
const { issueKanBanView: issueKanBanViewStore } = useMobxStore();
@ -182,6 +209,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={projects}
@ -194,6 +222,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -201,6 +230,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={states}
@ -213,6 +243,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -220,6 +251,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={stateGroups}
@ -232,6 +264,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -239,6 +272,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={priorities}
@ -251,6 +285,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -258,6 +293,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
@ -270,6 +306,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -277,6 +314,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={members ? [...members, { id: "None", display_name: "None" }] : members}
@ -289,6 +327,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
@ -296,6 +335,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
<GroupByKanBan
issues={issues}
group_by={group_by}
order_by={order_by}
sub_group_by={sub_group_by}
sub_group_id={sub_group_id}
list={members}
@ -308,6 +348,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
isDragStarted={isDragStarted}
/>
)}
</div>

View File

@ -17,7 +17,7 @@ export interface IStateGroupHeader {
}
export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) => (
<div className="w-[14px] h-[14px] rounded-full">
<div className="w-3.5 h-3.5 rounded-full">
<StateGroupIcon stateGroup={stateGroup} color={color || null} width="14" height="14" />
</div>
);

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { CycleIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
@ -37,6 +38,8 @@ export const CycleKanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
@ -45,7 +48,15 @@ export const CycleKanBanLayout: React.FC = observer(() => {
? "swimlanes"
: "default";
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const onDragStart = () => {
setIsDragStarted(true);
};
const onDragEnd = (result: any) => {
setIsDragStarted(false);
if (!result) return;
if (
@ -99,60 +110,72 @@ export const CycleKanBanLayout: React.FC = observer(() => {
: null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
<>
{cycleIssueStore.loader ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
)}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<CycleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
)}
</DragDropContext>
</div>
</DragDropContext>
</div>
)}
</>
);
});

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { ModuleIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
@ -36,6 +37,8 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
@ -44,7 +47,14 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
? "swimlanes"
: "default";
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const onDragStart = () => {
setIsDragStarted(true);
};
const onDragEnd = (result: any) => {
setIsDragStarted(false);
if (!result) return;
if (
@ -98,60 +108,72 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
: null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
<>
{moduleIssueStore.loader ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
)}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ModuleIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
)}
</DragDropContext>
</div>
</DragDropContext>
</div>
)}
</>
);
});

View File

@ -1,4 +1,4 @@
import { FC, useCallback } from "react";
import { FC, useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { DragDropContext } from "@hello-pangea/dnd";
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { ProjectIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
// types
@ -34,6 +35,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
const order_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.order_by || null;
const userDisplayFilters = profileIssueFiltersStore?.userDisplayFilters || null;
const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null;
@ -42,7 +45,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
? "swimlanes"
: "default";
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const onDragStart = () => {
setIsDragStarted(true);
};
const onDragEnd = (result: any) => {
setIsDragStarted(false);
if (!result) return;
if (
@ -83,58 +93,70 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
const projects = projectStore?.workspaceProjects || null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
<>
{profileIssuesStore.loader ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
)}
</DragDropContext>
</div>
</DragDropContext>
</div>
)}
</>
);
});

View File

@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
@ -8,6 +8,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { ProjectIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// types
import { IIssue } from "types";
// constants
@ -34,6 +35,8 @@ export const KanBanLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null;
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
@ -42,12 +45,22 @@ export const KanBanLayout: React.FC = observer(() => {
? "swimlanes"
: "default";
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const onDragStart = () => {
setIsDragStarted(true);
};
const onDragEnd = (result: any) => {
setIsDragStarted(false);
if (!result) return;
if (
result.destination &&
result.source &&
result.source.droppableId &&
result.destination.droppableId &&
result.destination.droppableId === result.source.droppableId &&
result.destination.index === result.source.index
)
@ -87,59 +100,71 @@ export const KanBanLayout: React.FC = observer(() => {
: null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
<>
{issueStore.loader ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
enableQuickIssueCreate
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
isDragStarted={isDragStarted}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
enableQuickIssueCreate
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(sub_group_by, group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
/>
)}
</DragDropContext>
</div>
</DragDropContext>
</div>
)}
</>
);
});

View File

@ -58,6 +58,7 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
};
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
order_by: string | null;
showEmptyGroup: boolean;
states: IState[] | null;
stateGroups: any;
@ -76,12 +77,14 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
isDragStarted?: boolean;
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const {
issues,
sub_group_by,
group_by,
order_by,
list,
listKey,
handleIssues,
@ -96,6 +99,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
labels,
members,
projects,
isDragStarted,
} = props;
const calculateIssueCount = (column_id: string) => {
@ -133,6 +137,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
issues={issues?.[getValueFromObject(_list, listKey) as string]}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
sub_group_id={getValueFromObject(_list, listKey) as string}
handleIssues={handleIssues}
quickActions={quickActions}
@ -147,6 +152,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
members={members}
projects={projects}
enableQuickIssueCreate
isDragStarted={isDragStarted}
/>
</div>
)}
@ -160,6 +166,7 @@ export interface IKanBanSwimLanes {
issues: any;
sub_group_by: string | null;
group_by: string | null;
order_by: string | null;
handleIssues: (
sub_group_by: string | null,
group_by: string | null,
@ -177,6 +184,7 @@ export interface IKanBanSwimLanes {
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
isDragStarted?: boolean;
}
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
@ -184,6 +192,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues,
sub_group_by,
group_by,
order_by,
handleIssues,
quickActions,
displayProperties,
@ -196,6 +205,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels,
members,
projects,
isDragStarted,
} = props;
return (
@ -291,6 +301,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={projects}
listKey={`id`}
handleIssues={handleIssues}
@ -305,6 +316,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -313,6 +325,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={states}
listKey={`id`}
handleIssues={handleIssues}
@ -327,6 +340,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -335,6 +349,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={states}
listKey={`id`}
handleIssues={handleIssues}
@ -349,6 +364,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -357,6 +373,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={stateGroups}
listKey={`key`}
handleIssues={handleIssues}
@ -371,6 +388,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -379,6 +397,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={priorities}
listKey={`key`}
handleIssues={handleIssues}
@ -393,6 +412,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -401,6 +421,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={labels ? [...labels, { id: "None", name: "None" }] : labels}
listKey={`id`}
handleIssues={handleIssues}
@ -415,6 +436,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -423,6 +445,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={members ? [...members, { id: "None", display_name: "None" }] : members}
listKey={`id`}
handleIssues={handleIssues}
@ -437,6 +460,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
@ -445,6 +469,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
list={members}
listKey={`id`}
handleIssues={handleIssues}
@ -459,6 +484,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
labels={labels}
members={members}
projects={projects}
isDragStarted={isDragStarted}
/>
)}
</div>

View File

@ -4,20 +4,20 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IIssue } from "types";
import { IIssue, IIssueDisplayProperties } from "types";
interface IssueBlockProps {
columnId: string;
issue: IIssue;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
isReadonly?: boolean;
showEmptyGroup?: boolean;
}
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
const { columnId, issue, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props;
const { columnId, issue, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props;
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
handleIssues(group_by, issueToUpdate, "update");
@ -26,7 +26,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
return (
<>
<div className="text-sm p-3 relative bg-custom-background-100 flex items-center gap-3">
{display_properties && display_properties?.key && (
{displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs text-custom-text-300 font-medium">
{issue?.project_detail?.identifier}-{issue.sequence_id}
</div>
@ -54,7 +54,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
issue={issue}
isReadonly={isReadonly}
handleIssues={updateIssue}
display_properties={display_properties}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
/>
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}

View File

@ -2,7 +2,7 @@ import { FC } from "react";
// components
import { IssueBlock } from "components/issues";
// types
import { IIssue } from "types";
import { IIssue, IIssueDisplayProperties } from "types";
interface Props {
columnId: string;
@ -10,12 +10,12 @@ interface Props {
isReadonly?: boolean;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
showEmptyGroup?: boolean;
}
export const IssueBlocksList: FC<Props> = (props) => {
const { columnId, issues, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props;
const { columnId, issues, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props;
return (
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
@ -28,7 +28,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
handleIssues={handleIssues}
quickActions={quickActions}
isReadonly={isReadonly}
display_properties={display_properties}
displayProperties={displayProperties}
showEmptyGroup={showEmptyGroup}
/>
))

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite";
import { ListGroupByHeaderRoot } from "./headers/group-by-root";
import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
// constants
import { getValueFromObject } from "constants/issue";
@ -16,7 +16,7 @@ export interface IGroupByList {
listKey: string;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
is_list?: boolean;
enableQuickIssueCreate?: boolean;
showEmptyGroup?: boolean;
@ -31,7 +31,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
listKey,
handleIssues,
quickActions,
display_properties,
displayProperties,
is_list = false,
enableQuickIssueCreate,
showEmptyGroup,
@ -59,7 +59,7 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
issues={is_list ? issues : issues[getValueFromObject(_list, listKey) as string]}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
/>
@ -86,7 +86,7 @@ export interface IList {
handleDragDrop?: (result: any) => void | undefined;
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
@ -105,7 +105,7 @@ export const List: React.FC<IList> = observer((props) => {
isReadonly,
handleIssues,
quickActions,
display_properties,
displayProperties,
states,
labels,
members,
@ -126,7 +126,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
is_list
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
@ -142,7 +142,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -157,7 +157,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -172,7 +172,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -187,7 +187,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`key`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -202,7 +202,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -217,7 +217,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}
@ -232,7 +232,7 @@ export const List: React.FC<IList> = observer((props) => {
listKey={`id`}
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
displayProperties={displayProperties}
enableQuickIssueCreate={enableQuickIssueCreate}
isReadonly={isReadonly}
showEmptyGroup={showEmptyGroup}

View File

@ -91,12 +91,12 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
/>
)}
<div
className={`flex-shrink-0 relative flex gap-2 p-1.5 ${
verticalAlignPosition ? `flex-col items-center w-[44px]` : `flex-row items-center w-full`
className={`flex-shrink-0 relative flex gap-2 py-1.5 ${
verticalAlignPosition ? `flex-col items-center w-11` : `flex-row items-center w-full`
}`}
>
<div className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center">
{icon ? icon : <CircleDashed width={14} strokeWidth={2} />}
<div className="flex-shrink-0 w-5 h-5 rounded-sm overflow-hidden flex justify-center items-center">
{icon ? icon : <CircleDashed className="h-3.5 w-3.5" strokeWidth={2} />}
</div>
<div className={`flex items-center gap-1 ${verticalAlignPosition ? `flex-col` : `flex-row w-full`}`}>
@ -114,8 +114,8 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
<CustomMenu
width="auto"
customButton={
<span className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
<Plus width={14} strokeWidth={2} />
<span className="flex-shrink-0 w-5 h-5 rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all">
<Plus className="h-3.5 w-3.5" strokeWidth={2} />
</span>
}
>
@ -128,7 +128,7 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
</CustomMenu>
) : (
<div
className="flex-shrink-0 w-[20px] h-[20px] rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
className="flex-shrink-0 w-5 h-5 rounded-sm overflow-hidden flex justify-center items-center hover:bg-custom-background-80 cursor-pointer transition-all"
onClick={() => setIsOpen(true)}
>
<Plus width={14} strokeWidth={2} />

View File

@ -11,19 +11,19 @@ import { IssuePropertyDate } from "../properties/date";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IIssue, IState, TIssuePriorities } from "types";
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
export interface IKanBanProperties {
columnId: string;
issue: IIssue;
handleIssues: (group_by: string | null, issue: IIssue) => void;
display_properties: any;
displayProperties: IIssueDisplayProperties;
isReadonly?: boolean;
showEmptyGroup?: boolean;
}
export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
const { columnId: group_id, issue, handleIssues, display_properties, isReadonly, showEmptyGroup } = props;
const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props;
const handleState = (state: IState) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id });
@ -57,7 +57,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
<div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
{/* basic properties */}
{/* state */}
{display_properties && display_properties?.state && (
{displayProperties && displayProperties?.state && (
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null}
@ -68,7 +68,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* priority */}
{display_properties && display_properties?.priority && (
{displayProperties && displayProperties?.priority && (
<IssuePropertyPriority
value={issue?.priority || null}
onChange={handlePriority}
@ -78,7 +78,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* label */}
{display_properties && display_properties?.labels && (showEmptyGroup || issue?.labels.length > 0) && (
{displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && (
<IssuePropertyLabels
projectId={issue?.project_detail?.id || null}
value={issue?.labels || null}
@ -89,7 +89,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* assignee */}
{display_properties && display_properties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && (
{displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && (
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
@ -101,7 +101,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* start date */}
{display_properties && display_properties?.start_date && (showEmptyGroup || issue?.start_date) && (
{displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && (
<IssuePropertyDate
value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)}
@ -111,7 +111,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* target/due date */}
{display_properties && display_properties?.due_date && (showEmptyGroup || issue?.target_date) && (
{displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && (
<IssuePropertyDate
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)}
@ -121,7 +121,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* estimates */}
{display_properties && display_properties?.estimate && (
{displayProperties && displayProperties?.estimate && (
<IssuePropertyEstimates
projectId={issue?.project_detail?.id || null}
value={issue?.estimate_point || null}
@ -133,7 +133,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
{/* extra render properties */}
{/* sub-issues */}
{display_properties && display_properties?.sub_issue_count && (
{displayProperties && displayProperties?.sub_issue_count && (
<Tooltip tooltipHeading="Sub-issues" tooltipContent={`${issue.sub_issues_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Layers className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@ -143,7 +143,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* attachments */}
{display_properties && display_properties?.attachment_count && (
{displayProperties && displayProperties?.attachment_count && (
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
@ -153,7 +153,7 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* link */}
{display_properties && display_properties?.link && (
{displayProperties && displayProperties?.link && (
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex-shrink-0 border-[0.5px] border-custom-border-300 overflow-hidden rounded flex justify-center items-center gap-2 px-2.5 py-1 h-5">
<Link className="h-3 w-3 flex-shrink-0" strokeWidth={2} />

View File

@ -25,7 +25,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
// derived values
const issues = archivedIssueStore.getIssues;
const display_properties = archivedIssueFiltersStore?.userDisplayProperties || null;
const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null;
const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null;
const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => {
@ -59,7 +59,7 @@ export const ArchivedIssueListLayout: FC = observer(() => {
quickActions={(group_by, issue) => (
<ArchivedIssueQuickActions issue={issue} handleDelete={async () => handleIssues(group_by, issue, "delete")} />
)}
display_properties={display_properties}
displayProperties={displayProperties}
states={states}
stateGroups={stateGroups}
priorities={priorities}

View File

@ -31,7 +31,7 @@ export const CycleListLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const handleIssues = useCallback(
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
@ -80,7 +80,7 @@ export const CycleListLayout: React.FC = observer(() => {
handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
states={states}
stateGroups={stateGroups}
priorities={priorities}

View File

@ -31,7 +31,7 @@ export const ModuleListLayout: React.FC = observer(() => {
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const handleIssues = useCallback(
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => {
@ -80,7 +80,7 @@ export const ModuleListLayout: React.FC = observer(() => {
handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
states={states}
stateGroups={stateGroups}
priorities={priorities}

View File

@ -29,7 +29,7 @@ export const ProfileIssuesListLayout: FC = observer(() => {
const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null;
const display_properties = profileIssueFiltersStore?.userDisplayProperties || null;
const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null;
const handleIssues = useCallback(
(group_by: string | null, issue: IIssue, action: "update" | "delete") => {
@ -64,7 +64,7 @@ export const ProfileIssuesListLayout: FC = observer(() => {
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
display_properties={display_properties}
displayProperties={displayProperties}
states={states}
stateGroups={stateGroups}
priorities={priorities}

View File

@ -6,6 +6,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components
import { List } from "../default";
import { ProjectIssueQuickActions } from "components/issues";
import { Spinner } from "@plane/ui";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
@ -29,7 +30,7 @@ export const ListLayout: FC = observer(() => {
const userDisplayFilters = issueFilterStore?.userDisplayFilters || null;
const group_by: string | null = userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const displayProperties = issueFilterStore?.userDisplayProperties || null;
const handleIssues = useCallback(
(group_by: string | null, issue: IIssue, action: "update" | "delete") => {
@ -56,29 +57,37 @@ export const ListLayout: FC = observer(() => {
: null;
return (
<div className="relative w-full h-full bg-custom-background-90">
<List
issues={issues}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
<>
{issueStore.loader ? (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
) : (
<div className="relative w-full h-full bg-custom-background-90">
<List
issues={issues}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<ProjectIssueQuickActions
issue={issue}
handleDelete={async () => handleIssues(group_by, issue, "delete")}
handleUpdate={async (data) => handleIssues(group_by, data, "update")}
/>
)}
displayProperties={displayProperties}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
enableQuickIssueCreate
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
showEmptyGroup={userDisplayFilters.show_empty_groups}
/>
)}
display_properties={display_properties}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
enableQuickIssueCreate
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
showEmptyGroup={userDisplayFilters.show_empty_groups}
/>
</div>
</div>
)}
</>
);
});

View File

@ -1,16 +1,13 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { usePopper } from "react-popper";
import useEstimateOption from "hooks/use-estimate-option";
// ui
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
import { observer } from "mobx-react-lite";
import { Combobox } from "@headlessui/react";
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
// ui
import { Tooltip } from "@plane/ui";
// types
import { Placement } from "@popperjs/core";
import { useMobxStore } from "lib/mobx/store-provider";
export interface IIssuePropertyEstimates {
view?: "profile" | "workspace" | "project";
@ -27,7 +24,6 @@ export interface IIssuePropertyEstimates {
export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observer((props) => {
const {
view,
projectId,
value,
onChange,
@ -44,8 +40,6 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { isEstimateActive, estimatePoints } = useEstimateOption();
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
@ -58,6 +52,14 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
],
});
const { project: projectStore } = useMobxStore();
const projectDetails = projectId ? projectStore.project_details[projectId] : null;
const isEstimateEnabled = projectDetails?.estimate !== null;
const estimates = projectId ? projectStore.estimates?.[projectId] : null;
const estimatePoints =
projectDetails && isEstimateEnabled ? estimates?.find((e) => e.id === projectDetails.estimate)?.points : null;
const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map(
(estimate) => ({
value: estimate.key,
@ -94,6 +96,8 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
</Tooltip>
);
if (!isEstimateEnabled) return null;
return (
<Combobox
as="div"

View File

@ -20,11 +20,7 @@ import { Spinner } from "@plane/ui";
// helpers
import { getDateRangeStatus } from "helpers/date-time.helper";
type Props = {
openIssuesListModal: () => void;
};
export const CycleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal }) => {
export const CycleLayoutRoot: React.FC = observer(() => {
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
const router = useRouter();
@ -73,7 +69,11 @@ export const CycleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
<CycleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<CycleEmptyState openIssuesListModal={openIssuesListModal} />
<CycleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
cycleId={cycleId?.toString()}
/>
) : (
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (

View File

@ -18,11 +18,7 @@ import {
// ui
import { Spinner } from "@plane/ui";
type Props = {
openIssuesListModal: () => void;
};
export const ModuleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal }) => {
export const ModuleLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query as {
workspaceSlug: string;
@ -66,7 +62,11 @@ export const ModuleLayoutRoot: React.FC<Props> = observer(({ openIssuesListModal
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ModuleAppliedFiltersRoot />
{(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? (
<ModuleEmptyState openIssuesListModal={openIssuesListModal} />
<ModuleEmptyState
workspaceSlug={workspaceSlug?.toString()}
projectId={projectId?.toString()}
moduleId={moduleId?.toString()}
/>
) : (
<div className="h-full w-full overflow-auto">
{activeLayout === "list" ? (

View File

@ -22,13 +22,19 @@ export const ProjectLayoutRoot: React.FC = observer(() => {
const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore();
useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
const { isLoading } = useSWR(
workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
}
}
});
);
console.log("--");
console.log("isLoading -- -->", isLoading);
console.log("--");
const activeLayout = issueFilterStore.userDisplayFilters.layout;

View File

@ -49,7 +49,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
const [isEditing, setIsEditing] = useState(false);
const editorSuggestions = useEditorSuggestions(workspaceSlug, projectId);
const editorSuggestions = useEditorSuggestions();
const {
formState: { isSubmitting },

View File

@ -51,9 +51,9 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
const editorRef = React.useRef<any>(null);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { workspaceSlug } = router.query;
const editorSuggestions = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined);
const editorSuggestions = useEditorSuggestions();
const {
control,
@ -90,6 +90,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
ref={editorRef}
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
customClassName="p-2 h-full"
editorContentCustomClassNames="min-h-[35px]"
debouncedUpdatesEnabled={false}
mentionSuggestions={editorSuggestions.mentionSuggestions}
mentionHighlights={editorSuggestions.mentionHighlights}

View File

@ -32,13 +32,13 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
// store
const { user: userStore } = useMobxStore();
const { currentProjectRole } = userStore;
const isAllowed = [5, 10].includes(currentProjectRole || 0);
const isAllowed = [15, 20].includes(currentProjectRole || 0);
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [characterLimit, setCharacterLimit] = useState(false);
// hooks
const { setShowAlert } = useReloadConfirmations();
const editorSuggestions = useEditorSuggestions(workspaceSlug, issue.project_detail.id);
const editorSuggestions = useEditorSuggestions();
const {
handleSubmit,

View File

@ -74,13 +74,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
};
const addIssueToCycle = async (cycleId: string) => {
if (!workspaceSlug || !issue || !cycleId) return;
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, issue.id);
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), issue.project_detail.id, cycleId, [issue.id]);
};
const addIssueToModule = async (moduleId: string) => {
if (!workspaceSlug || !issue || !moduleId) return;
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, issue.id);
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), issue.project_detail.id, moduleId, [issue.id]);
};
const handleLabels = (formData: Partial<IIssue>) => {
issueUpdate({ ...issue, ...formData });

View File

@ -1,4 +1,4 @@
import { FC, ReactNode } from "react";
import { FC, Fragment, ReactNode } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
@ -112,6 +112,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
else await issueStore.deleteIssue(workspaceSlug, projectId, issue!);
const { query } = router;
if (query.peekIssueId) {
issueDetailStore.setPeekId(null);
delete query.peekIssueId;
router.push({
pathname: router.pathname,
@ -123,30 +124,32 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const userRole = userStore.currentProjectRole ?? 5;
return (
<IssueView
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issue={issue}
isLoading={isLoading}
isArchived={isArchived}
handleCopyText={handleCopyText}
redirectToIssueDetail={redirectToIssueDetail}
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
handleDeleteIssue={handleDeleteIssue}
disableUserActions={[5, 10].includes(userRole)}
showCommentAccessSpecifier={projectStore.currentProjectDetails?.is_deployed}
>
{children}
</IssueView>
<Fragment>
<IssueView
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
issue={issue}
isLoading={isLoading}
isArchived={isArchived}
handleCopyText={handleCopyText}
redirectToIssueDetail={redirectToIssueDetail}
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
handleDeleteIssue={handleDeleteIssue}
disableUserActions={[5, 10].includes(userRole)}
showCommentAccessSpecifier={projectStore.currentProjectDetails?.is_deployed}
>
{children}
</IssueView>
</Fragment>
);
});

View File

@ -97,6 +97,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const updateRoutePeekId = () => {
if (issueId != peekIssueId) {
issueDetailStore.setPeekId(issueId);
const { query } = router;
router.push({
pathname: router.pathname,
@ -107,6 +108,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const removeRoutePeekId = () => {
const { query } = router;
if (query.peekIssueId) {
issueDetailStore.setPeekId(null);
delete query.peekIssueId;
router.push({
pathname: router.pathname,
@ -248,7 +250,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issue && (
<>
{["side-peek", "modal"].includes(peekMode) ? (
<div className="flex flex-col gap-3 px-10 py-6">
<div className="flex flex-col gap-3 py-6 px-8">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
@ -292,8 +294,6 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueReactionRemove={issueReactionRemove}
/>
<div className="border-t border-custom-border-400" />
<IssueComment
workspaceSlug={workspaceSlug}
projectId={projectId}

View File

@ -168,13 +168,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return;
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, issueId);
cycleIssueStore.addIssueToCycle(workspaceSlug.toString(), activeProject, cycleId, [issueId]);
};
const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !activeProject) return;
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, issueId);
moduleIssueStore.addIssueToModule(workspaceSlug.toString(), activeProject, moduleId, [issueId]);
};
const createIssue = async (payload: Partial<IIssue>) => {
@ -320,7 +320,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
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="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full sm:max-w-3xl">
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 p-5 text-left shadow-xl transition-all sm:w-full mx-4 sm:max-w-4xl">
<IssueForm
handleFormSubmit={handleFormSubmit}
initialData={data ?? prePopulateData}

View File

@ -16,7 +16,7 @@ import { getNumberCount } from "helpers/string.helper";
import { useMobxStore } from "lib/mobx/store-provider";
export const NotificationPopover = observer(() => {
const store: any = useMobxStore();
const { theme: themeStore } = useMobxStore();
const {
notifications,
@ -45,6 +45,8 @@ export const NotificationPopover = observer(() => {
markAllNotificationsAsRead,
} = useUserNotification();
const isSidebarCollapsed = themeStore.sidebarCollapsed;
return (
<>
<SnoozeNotificationModal
@ -62,23 +64,18 @@ export const NotificationPopover = observer(() => {
return (
<>
<Tooltip
tooltipContent="Notifications"
position="right"
className="ml-2"
disabled={!store?.theme?.sidebarCollapsed}
>
<Tooltip tooltipContent="Notifications" position="right" className="ml-2" disabled={!isSidebarCollapsed}>
<Popover.Button
className={`relative group flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium outline-none ${
isActive
? "bg-custom-primary-100/10 text-custom-primary-100"
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80"
} ${store?.theme?.sidebarCollapsed ? "justify-center" : ""}`}
} ${isSidebarCollapsed ? "justify-center" : ""}`}
>
<Bell className="h-4 w-4" />
{store?.theme?.sidebarCollapsed ? null : <span>Notifications</span>}
{isSidebarCollapsed ? null : <span>Notifications</span>}
{totalNotificationCount && totalNotificationCount > 0 ? (
store?.theme?.sidebarCollapsed ? (
isSidebarCollapsed ? (
<span className="absolute right-3.5 top-2 h-2 w-2 bg-custom-primary-300 rounded-full" />
) : (
<span className="ml-auto bg-custom-primary-300 rounded-full text-xs text-white px-1.5">

View File

@ -1,16 +1,15 @@
import { useState } from "react";
import Image from "next/image";
// hooks
import useUser from "hooks/use-user";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { TourSidebar } from "components/onboarding";
// ui
import { Button } from "@plane/ui";
// icons
import { X } from "lucide-react";
// images
// assets
import PlaneWhiteLogo from "public/plane-logos/white-horizontal.svg";
import IssuesTour from "public/onboarding/issues.webp";
import CyclesTour from "public/onboarding/cycles.webp";
@ -75,10 +74,13 @@ const TOUR_STEPS: {
},
];
export const TourRoot: React.FC<Props> = ({ onComplete }) => {
export const TourRoot: React.FC<Props> = observer((props) => {
const { onComplete } = props;
// states
const [step, setStep] = useState<TTourSteps>("welcome");
const { user } = useUser();
const { user: userStore, commandPalette: commandPaletteStore } = useMobxStore();
const user = userStore.currentUser;
const currentStepIndex = TOUR_STEPS.findIndex((tourStep) => tourStep.key === step);
const currentStep = TOUR_STEPS[currentStepIndex];
@ -91,7 +93,7 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
<div className="h-3/5 bg-custom-primary-100 grid place-items-center">
<Image src={PlaneWhiteLogo} alt="Plane White Logo" />
</div>
<div className="h-2/5 overflow-y-auto p-6">
<div className="h-2/5 flex flex-col overflow-y-auto p-6">
<h3 className="font-semibold sm:text-xl">
Welcome to Plane, {user?.first_name} {user?.last_name}
</h3>
@ -99,17 +101,19 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
We{"'"}re glad that you decided to try out Plane. You can now manage your projects with ease. Get
started by creating a project.
</p>
<div className="flex items-center gap-6 mt-8">
<Button variant="primary" onClick={() => setStep("issues")}>
Take a Product Tour
</Button>
<button
type="button"
className="outline-custom-text-100 bg-transparent text-custom-primary-100 text-xs font-medium"
onClick={onComplete}
>
No thanks, I will explore it myself
</button>
<div className="flex items-end h-full">
<div className="flex items-center gap-6 mt-8">
<Button variant="primary" onClick={() => setStep("issues")}>
Take a Product Tour
</Button>
<button
type="button"
className="outline-custom-text-100 bg-transparent text-custom-primary-100 text-xs font-medium"
onClick={onComplete}
>
No thanks, I will explore it myself
</button>
</div>
</div>
</div>
</div>
@ -148,8 +152,14 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
</Button>
)}
</div>
{TOUR_STEPS.findIndex((tourStep) => tourStep.key === step) === TOUR_STEPS.length - 1 && (
<Button variant="primary" onClick={onComplete}>
{currentStepIndex === TOUR_STEPS.length - 1 && (
<Button
variant="primary"
onClick={() => {
onComplete();
commandPaletteStore.toggleCreateProjectModal(true);
}}
>
Create my first project
</Button>
)}
@ -160,4 +170,4 @@ export const TourRoot: React.FC<Props> = ({ onComplete }) => {
)}
</>
);
};
});

View File

@ -1 +1,2 @@
export * from "./signin";
export * from "./workspace-dashboard";

View File

@ -56,7 +56,7 @@ export const CreateUpdateBlockInline: FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined)
const editorSuggestion = useEditorSuggestions();
const { setToastAlert } = useToast();

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import Link from "next/link";
import { mutate } from "swr";
import { useForm } from "react-hook-form";
import { Draggable } from "react-beautiful-dnd";
import { Draggable } from "@hello-pangea/dnd";
// services
import { PageService } from "services/page.service";
import { IssueService } from "services/issue/issue.service";
@ -64,7 +64,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, showBl
},
});
const editorSuggestion = useEditorSuggestions(workspaceSlug as string | undefined, projectId as string | undefined)
const editorSuggestion = useEditorSuggestions();
const updatePageBlock = async (formData: Partial<IPageBlock>) => {
if (!workspaceSlug || !projectId || !pageId) return;

View File

@ -211,7 +211,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = observer((props) => {
<div className="flex items-center">
<Button
variant="link-primary"
className="!p-0"
className="!p-0 font-semibold"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View File

@ -261,6 +261,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
name="name"
type="text"
value={value}
tabIndex={1}
onChange={handleNameChange(onChange)}
hasError={Boolean(errors.name)}
placeholder="Project Title"
@ -293,6 +294,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
name="identifier"
type="text"
value={value}
tabIndex={2}
onChange={handleIdentifierChange(onChange)}
hasError={Boolean(errors.identifier)}
placeholder="Identifier"
@ -311,6 +313,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
id="description"
name="description"
value={value}
tabIndex={3}
placeholder="Description..."
onChange={onChange}
className="text-sm !h-24"
@ -322,7 +325,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex-shrink-0">
<div className="flex-shrink-0" tabIndex={4}>
<Controller
name="network"
control={control}
@ -359,7 +362,7 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
)}
/>
</div>
<div className="flex-shrink-0">
<div className="flex-shrink-0" tabIndex={5}>
<Controller
name="project_lead_member"
control={control}
@ -377,10 +380,10 @@ export const CreateProjectModal: FC<Props> = observer((props) => {
</div>
<div className="flex justify-end gap-2 pt-5">
<Button variant="neutral-primary" onClick={handleClose}>
<Button variant="neutral-primary" onClick={handleClose} tabIndex={6}>
Cancel
</Button>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting}>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}>
{isSubmitting ? "Creating..." : "Create Project"}
</Button>
</div>

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
import { DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// icons

View File

@ -1,6 +1,6 @@
import React, { useState, FC, useRef, useEffect } from "react";
import { useRouter } from "next/router";
import { DragDropContext, Draggable, DropResult, Droppable } from "react-beautiful-dnd";
import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd";
import { Disclosure, Transition } from "@headlessui/react";
import { observer } from "mobx-react-lite";
// hooks

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,3 @@ export * from "./form";
export * from "./modal";
export * from "./view-list-item";
export * from "./views-list";
export * from "./workspace-dashboard";

View File

@ -1,9 +1,7 @@
import { IMentionHighlight, IMentionSuggestion } from "@plane/rich-text-editor";
import useUser from "./use-user";
import { useMobxStore } from "lib/mobx/store-provider";
import { RootStore } from "store/root";
const useEditorSuggestions = (_workspaceSlug: string | undefined, _projectId: string | undefined) => {
const useEditorSuggestions = () => {
const { mentionsStore }: RootStore = useMobxStore();
return {

View File

@ -11,14 +11,10 @@
"export": "next export"
},
"dependencies": {
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.3",
"@hello-pangea/dnd": "^16.3.0",
"@jitsu/nextjs": "^3.1.5",
"@mui/material": "^5.14.1",
"@nivo/bar": "0.80.0",
"@nivo/calendar": "0.80.0",
"@nivo/core": "0.80.0",
@ -34,13 +30,10 @@
"@types/lodash.debounce": "^4.0.7",
"@types/react-datepicker": "^4.8.0",
"axios": "^1.1.3",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"dotenv": "^16.0.3",
"highlight.js": "^11.8.0",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lowlight": "^2.9.0",
"lucide-react": "^0.274.0",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
@ -49,29 +42,24 @@
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-color": "^2.19.3",
"react-datepicker": "^4.8.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.38.0",
"react-markdown": "^8.0.7",
"react-moveable": "^0.54.1",
"react-popper": "^2.3.0",
"sharp": "^0.32.1",
"swr": "^2.1.3",
"tailwind-merge": "^1.14.0",
"tlds": "^1.238.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
"@types/node": "18.0.6",
"@types/nprogress": "^0.2.0",
"@types/react": "18.0.15",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react": "^18.2.35",
"@types/react-color": "^3.0.6",
"@types/react-dom": "18.0.6",
"@types/react-dom": "^18.2.14",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",

View File

@ -2,7 +2,7 @@ import { ReactElement } from "react";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { WorkspaceDashboardView } from "components/views";
import { WorkspaceDashboardView } from "components/page-views";
import { WorkspaceDashboardHeader } from "components/headers/workspace-dashboard";
// types
import { NextPageWithLayout } from "types/app";

View File

@ -1,19 +1,14 @@
import { useState, ReactElement } from "react";
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { IssueService } from "services/issue";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { CycleIssuesHeader } from "components/headers";
import { ExistingIssuesListModal } from "components/core";
import { CycleDetailsSidebar } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts";
// ui
@ -21,23 +16,14 @@ import { EmptyState } from "components/common";
// assets
import emptyCycle from "public/empty-state/cycle.svg";
// types
import { ISearchIssueResponse } from "types";
import { NextPageWithLayout } from "types/app";
const issueService = new IssueService();
const CycleDetailPage: NextPageWithLayout = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { cycle: cycleStore } = useMobxStore();
const { user } = useUser();
const { setToastAlert } = useToast();
const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
@ -52,38 +38,8 @@ const CycleDetailPage: NextPageWithLayout = () => {
setValue(`${!isSidebarCollapsed}`);
};
// TODO: add this function to bulk add issues to cycle
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
const payload = {
issues: data.map((i) => i.id),
};
await issueService
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user)
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the cycle. Please try again.",
});
});
};
const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
return (
<>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal
isOpen={cycleIssuesListModal}
handleClose={() => setCycleIssuesListModal(false)}
searchParams={{ cycle: true }}
handleOnSubmit={handleAddIssuesToCycle}
/>
{error ? (
<EmptyState
image={emptyCycle}
@ -98,7 +54,7 @@ const CycleDetailPage: NextPageWithLayout = () => {
<>
<div className="flex h-full w-full">
<div className="h-full w-full overflow-hidden">
<CycleLayoutRoot openIssuesListModal={openIssuesListModal} />
<CycleLayoutRoot />
</div>
{cycleId && !isSidebarCollapsed && (
<div

View File

@ -1,18 +1,13 @@
import { useState, ReactElement } from "react";
import { ReactElement } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// services
import { ModuleService } from "services/module.service";
// hooks
import useLocalStorage from "hooks/use-local-storage";
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { ExistingIssuesListModal } from "components/core";
import { ModuleDetailsSidebar } from "components/modules";
import { ModuleLayoutRoot } from "components/issues";
import { ModuleIssuesHeader } from "components/headers";
@ -22,21 +17,13 @@ import { EmptyState } from "components/common";
import emptyModule from "public/empty-state/module.svg";
// types
import { NextPageWithLayout } from "types/app";
import { ISearchIssueResponse } from "types";
const moduleService = new ModuleService();
const ModuleIssuesPage: NextPageWithLayout = () => {
// states
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
// store
const { module: moduleStore } = useMobxStore();
// hooks
const { user } = useUser();
const { setToastAlert } = useToast();
// local storage
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
@ -48,42 +35,12 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
: null
);
// TODO: add this function to bulk add issues to cycle
const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
const payload = {
issues: data.map((i) => i.id),
};
await moduleService
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user)
.catch(() =>
setToastAlert({
type: "error",
title: "Error!",
message: "Selected issues could not be added to the module. Please try again.",
})
);
};
const openIssuesListModal = () => {
setModuleIssuesListModal(true);
};
const toggleSidebar = () => {
setValue(`${!isSidebarCollapsed}`);
};
return (
<>
{/* TODO: Update logic to bulk add issues to a cycle */}
<ExistingIssuesListModal
isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)}
searchParams={{ module: true }}
handleOnSubmit={handleAddIssuesToModule}
/>
{error ? (
<EmptyState
image={emptyModule}
@ -97,7 +54,7 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
) : (
<div className="flex h-full w-full">
<div className="h-full w-full overflow-hidden">
<ModuleLayoutRoot openIssuesListModal={openIssuesListModal} />
<ModuleLayoutRoot />
</div>
{moduleId && !isSidebarCollapsed && (
<div

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