Compare commits

...

15 Commits

Author SHA1 Message Date
NarayanBavisetti
170f0da3c2 chore: priority migration 2023-09-12 23:21:55 +05:30
Aaryan Khandelwal
3a2a329000 fix: view props undefined (#2160) 2023-09-12 22:51:13 +05:30
Aaryan Khandelwal
8e9a4dca78 refactor: view props structure (#2159)
* chore: update view_props types

* refactor: view props structure
2023-09-12 22:27:15 +05:30
sriram veeraghanta
cdb888c23e fix: selfhosted fixes (#2154)
* fix: selfhosted fixes

* fix: updated env example
2023-09-12 20:32:26 +05:30
Dakshesh Jain
2186db8bba feat: users can select timezone during onboarding (#2148)
feat: using Intl timezone will be automatically selected but they have the option to change it
2023-09-12 13:35:15 +05:30
Bavisetti Narayan
9bff10de6d chore: changed issue priority from NULL to none (#2142)
* chore: changed issue priority from NULL to none

* fix: deleted the migration file
2023-09-12 13:06:49 +05:30
Henit Chobisa
6867154963 chore: added pre-release tag for workflow publications (#2133)
* chore: added pre-release tag for workflow publications

* chore: added backend services under a single image

* chore: exposed backend port for compose hub

* chore: removed backend exposed ports
2023-09-11 18:02:56 +05:30
Aaryan Khandelwal
7bb73b74ba refactor: priority icon component (#2132) 2023-09-11 14:35:58 +05:30
Dakshesh Jain
991258084e fix: query checking (#2137)
fix: the logic should be to check if object exist not if it's true or false
2023-09-11 13:21:50 +05:30
Thomas
1a37668f0b fix: husky was removed in commit #2086, but prepare still uses it (#2128) 2023-09-11 13:05:09 +05:30
M. Palanikannan
4447a4b519 fix: Tiptap comment card fix for space (#2129)
* fix:(space) fixed comment card's editor integration

* regression: removed content being set twice

* chore: added controller to manage tiptap editor

* chore: remove unused functions

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2023-09-11 12:54:19 +05:30
Dakshesh Jain
7842c4b2ea fix: authorize editor (#2122) 2023-09-11 12:24:46 +05:30
Aaryan Khandelwal
8de93d0081 chore: remove getServerSideProps (#2130) 2023-09-11 12:13:00 +05:30
Aaryan Khandelwal
5b228bd1eb chore: update state icons and colors (#2126)
* chore: update state icons and colors

* chore: update icons
2023-09-11 11:45:28 +05:30
Dakshesh Jain
ad8a011bb9 fix: issue activity (#2127) 2023-09-11 11:44:16 +05:30
125 changed files with 1885 additions and 1816 deletions

View File

@@ -23,6 +23,8 @@ NEXT_PUBLIC_SLACK_CLIENT_ID=""
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# public boards deploy url
NEXT_PUBLIC_DEPLOY_URL=""
# plane deploy using nginx
NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
# Backend
# Debug value for api server use it as 0 for production use

View File

@@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
on:
release:
types: [released]
types: [released, prereleased]
jobs:
build_push_backend:

View File

@@ -178,7 +178,7 @@ class IssueViewSet(BaseViewSet):
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -331,7 +331,7 @@ class UserWorkSpaceIssues(BaseAPIView):
try:
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -1068,7 +1068,7 @@ class IssueArchiveViewSet(BaseViewSet):
show_sub_issues = request.GET.get("show_sub_issues", "true")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -2078,7 +2078,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")

View File

@@ -1072,7 +1072,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.order_by("state_group")
)
priority_order = ["urgent", "high", "medium", "low", None]
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_distribution = (
Issue.issue_objects.filter(

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.2.3 on 2023-09-12 17:33
from django.db import migrations
def update_issue_priority_choice(apps, schema_editor):
IssueModel = apps.get_model("db", "Issue")
updated_issues = []
for obj in IssueModel.objects.all():
if obj.priority is None:
obj.priority = "none"
updated_issues.append(obj)
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0042_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.RunPython(update_issue_priority_choice),
]

View File

@@ -38,6 +38,7 @@ class Issue(ProjectBaseModel):
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None")
)
parent = models.ForeignKey(
"self",
@@ -64,8 +65,7 @@ class Issue(ProjectBaseModel):
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
null=True,
blank=True,
default="none",
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)

View File

@@ -1,6 +1,7 @@
from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
@@ -23,7 +24,6 @@ def filter_state_group(params, filter, method):
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
@@ -39,25 +39,10 @@ def filter_priority(params, filter, method):
if method == "GET":
priorities = params.get("priority").split(",")
if len(priorities) and "" not in priorities:
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
filter["priority__in"] = priorities
else:
if params.get("priority", None) and len(params.get("priority")):
priorities = params.get("priority")
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
filter["priority__in"] = params.get("priority")
return filter
@@ -229,7 +214,6 @@ def filter_issue_state_type(params, filter, method):
return filter
def filter_project(params, filter, method):
if method == "GET":
projects = params.get("project").split(",")
@@ -329,7 +313,7 @@ def issue_filters(query_params, method):
"module": filter_module,
"inbox_status": filter_inbox_status,
"sub_issue": filter_sub_issue_toggle,
"subscriber": filter_subscribed_issues,
"subscriber": filter_subscribed_issues,
"start_target_date": filter_start_target_date_issues,
}

View File

@@ -85,7 +85,7 @@ services:
plane-worker:
container_name: planebgworker
image: makeplane/plane-worker:latest
image: makeplane/plane-backend:latest
restart: always
command: ./bin/worker
env_file:
@@ -99,7 +99,7 @@ services:
plane-beat-worker:
container_name: planebeatworker
image: makeplane/plane-worker:latest
image: makeplane/plane-backend:latest
restart: always
command: ./bin/beat
env_file:

View File

@@ -90,8 +90,6 @@ services:
DOCKER_BUILDKIT: 1
restart: always
command: ./bin/takeoff
ports:
- 8000:8000
env_file:
- .env
environment:

View File

@@ -8,7 +8,6 @@
"packages/*"
],
"scripts": {
"prepare": "husky install",
"build": "turbo run build",
"dev": "turbo run dev",
"start": "turbo run start",

View File

@@ -12,4 +12,4 @@ fi
# Only perform action if $FROM and $TO are different.
echo "Replacing all statically built instances of $FROM with this string $TO ."
grep -R -la "${FROM}" apps/$DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"
grep -R -la "${FROM}" $DIRECTORY/.next | xargs -I{} sed -i "s|$FROM|$TO|g" "{}"

2
space/additional.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
// additional.d.ts
/// <reference types="next-images" />

View File

@@ -13,7 +13,7 @@ import useToast from "hooks/use-toast";
// components
import { EmailPasswordForm, GithubLoginButton, GoogleLoginButton, EmailCodeForm } from "components/accounts";
// images
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
export const SignInView = observer(() => {
const { user: userStore } = useMobxStore();
@@ -112,7 +112,7 @@ export const SignInView = observer(() => {
<div className="fixed grid place-items-center bg-custom-background-100 sm:py-5 top-11 sm:top-12 left-7 sm:left-16 lg:left-28">
<div className="grid place-items-center bg-custom-background-100">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane Logo" />
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane Logo" />
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@ import React, { useState } from "react";
// mobx
import { observer } from "mobx-react-lite";
// react-hook-form
import { useForm, Controller } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
// headless ui
import { Menu, Transition } from "@headlessui/react";
// lib
@@ -30,10 +30,13 @@ export const CommentCard: React.FC<Props> = observer((props) => {
// states
const [isEditing, setIsEditing] = useState(false);
const editorRef = React.useRef<any>(null);
const showEditorRef = React.useRef<any>(null);
const {
control,
formState: { isSubmitting },
handleSubmit,
control,
} = useForm<any>({
defaultValues: { comment_html: comment.comment_html },
});
@@ -47,6 +50,9 @@ export const CommentCard: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !issueDetailStore.peekId) return;
issueDetailStore.updateIssueComment(workspaceSlug, comment.project, issueDetailStore.peekId, comment.id, formData);
setIsEditing(false);
editorRef.current?.setEditorValue(formData.comment_html);
showEditorRef.current?.setEditorValue(formData.comment_html);
};
return (
@@ -96,6 +102,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
render={({ field: { onChange, value } }) => (
<TipTapEditor
workspaceSlug={workspaceSlug as string}
ref={editorRef}
value={value}
debouncedUpdatesEnabled={false}
customClassName="min-h-[50px] p-3 shadow-sm"
@@ -125,7 +132,8 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</form>
<div className={`${isEditing ? "hidden" : ""}`}>
<TipTapEditor
workspaceSlug={workspaceSlug.toString()}
workspaceSlug={workspaceSlug as string}
ref={showEditorRef}
value={comment.comment_html}
editable={false}
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"

View File

@@ -44,7 +44,6 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({ issueDetails, mod
{mode === "full" && (
<div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium">
{/* {getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)} */}
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h6>
<div className="flex items-center gap-2">

View File

@@ -56,12 +56,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
},
});
useEffect(() => {
if (editor) {
editor.commands.setContent(value);
}
}, [value]);
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({

View File

@@ -12,7 +12,10 @@ const nextConfig = {
};
if (parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) {
const nextConfigWithNginx = withImages({ basePath: "/spaces", ...nextConfig });
const nextConfigWithNginx = withImages({
basePath: "/spaces",
...nextConfig,
});
module.exports = nextConfigWithNginx;
} else {
module.exports = nextConfig;

View File

@@ -69,6 +69,7 @@
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"autoprefixer": "^10.4.13",
"eslint": "8.34.0",
"eslint-config-custom": "*",

View File

@@ -1,7 +1,8 @@
import useSWR from "swr";
import type { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import Head from "next/head";
import { useRouter } from "next/router";
import useSWR from "swr";
/// layouts
import ProjectLayout from "layouts/project-layout";
// components
@@ -39,12 +40,4 @@ const WorkspaceProjectPage = (props: any) => {
);
};
// export const getServerSideProps: GetServerSideProps<any> = async ({ query: { workspace_slug, project_slug } }) => {
// const res = await fetch(
// `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`
// );
// const project_settings = await res.json();
// return { props: { project_settings } };
// };
export default WorkspaceProjectPage;

View File

@@ -1,7 +1,4 @@
import React, { useEffect } from "react";
import Image from "next/image";
// assets
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
// mobx
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
@@ -12,6 +9,8 @@ import useToast from "hooks/use-toast";
// components
import { OnBoardingForm } from "components/accounts/onboarding-form";
const imagePrefix = process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX ? "/spaces/" : "";
const OnBoardingPage = () => {
const { user: userStore } = useMobxStore();
@@ -34,7 +33,7 @@ const OnBoardingPage = () => {
<div className="absolute border-b-[0.5px] sm:border-r-[0.5px] border-custom-border-200 h-[0.5px] w-full top-1/2 left-0 -translate-y-1/2 sm:h-screen sm:w-[0.5px] sm:top-0 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 sm:translate-y-0 z-10" />
<div className="absolute grid place-items-center bg-custom-background-100 px-3 sm:px-0 py-5 left-2 sm:left-1/2 md:left-1/3 sm:-translate-x-1/2 top-1/2 -translate-y-1/2 sm:translate-y-0 sm:top-12 z-10">
<div className="h-[30px] w-[30px]">
<Image src={BluePlaneLogoWithoutText} alt="Plane logo" />
<img src={`${imagePrefix}/plane-logos/blue-without-text.png`} alt="Plane logo" />
</div>
</div>
<div className="absolute sm:fixed text-custom-text-100 text-sm font-medium right-4 top-1/4 sm:top-12 -translate-y-1/2 sm:translate-y-0 sm:right-16 sm:py-5">

View File

@@ -1,6 +1,6 @@
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": ".",

View File

@@ -1,13 +1,13 @@
// nivo
import { BarDatum } from "@nivo/bar";
// icons
import { getPriorityIcon } from "components/icons";
import { PriorityIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
// constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
>
<div className="flex items-center gap-2">
{params.segment === "priority" ? (
getPriorityIcon(key)
<PriorityIcon priority={key as TIssuePriorities} />
) : (
<span
className="h-3 w-3 flex-shrink-0 rounded"
@@ -91,7 +91,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}`}
>
{params.x_axis === "priority" ? (
getPriorityIcon(`${item.name}`)
<PriorityIcon priority={item.name as TIssuePriorities} />
) : (
<span
className="h-3 w-3 rounded"

View File

@@ -1,7 +1,7 @@
// icons
import { PlayIcon } from "@heroicons/react/24/outline";
// types
import { IDefaultAnalyticsResponse } from "types";
import { IDefaultAnalyticsResponse, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<span
className="h-2 w-2 rounded-full"
style={{
backgroundColor: STATE_GROUP_COLORS[group.state_group],
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
}}
/>
<h6 className="capitalize">{group.state_group}</h6>
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
className="absolute top-0 left-0 h-1 rounded duration-300"
style={{
width: `${percentage}%`,
backgroundColor: STATE_GROUP_COLORS[group.state_group],
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
}}
/>
</div>

View File

@@ -9,7 +9,7 @@ import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// services
import stateService from "services/state.service";
// constants
@@ -46,7 +46,7 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
<StateGroupIcon stateGroup={state.group} color={state.color} height="16px" width="16px" />
{state.name}
</div>
),
@@ -140,14 +140,19 @@ export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleCha
label={
<div className="flex items-center gap-2">
{selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
<StateGroupIcon
stateGroup={selectedOption.group}
color={selectedOption.color}
height="16px"
width="16px"
/>
) : currentDefaultState ? (
getStateGroupIcon(
currentDefaultState.group,
"16",
"16",
currentDefaultState.color
)
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
height="16px"
width="16px"
/>
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}

View File

@@ -1,5 +1,7 @@
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import { mutate } from "swr";
// cmdk
@@ -7,12 +9,12 @@ import { Command } from "cmdk";
// services
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue } from "types";
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { PRIORITIES } from "constants/project";
// icons
import { CheckIcon, getPriorityIcon } from "components/icons";
import { CheckIcon, PriorityIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -54,7 +56,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
[workspaceSlug, issueId, projectId, user]
);
const handleIssueState = (priority: string | null) => {
const handleIssueState = (priority: TIssuePriorities) => {
submitChanges({ priority });
setIsPaletteOpen(false);
};
@@ -68,7 +70,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
{getPriorityIcon(priority)}
<PriorityIcon priority={priority} />
<span className="capitalize">{priority ?? "None"}</span>
</div>
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>

View File

@@ -1,22 +1,24 @@
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import useSWR, { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// ui
import { Spinner } from "components/ui";
// helpers
import { getStatesList } from "helpers/state.helper";
// services
import issuesService from "services/issues.service";
import stateService from "services/state.service";
// ui
import { Spinner } from "components/ui";
// icons
import { CheckIcon, StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { ICurrentUserResponse, IIssue } from "types";
// fetch keys
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY, STATES_LIST } from "constants/fetch-keys";
// icons
import { CheckIcon, getStateGroupIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -82,7 +84,12 @@ export const ChangeIssueState: React.FC<Props> = ({ setIsPaletteOpen, issue, use
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
{getStateGroupIcon(state.group, "16", "16", state.color)}
<StateGroupIcon
stateGroup={state.group}
color={state.color}
height="16px"
width="16px"
/>
<p>{state.name}</p>
</div>
<div>{state.id === issue.state && <CheckIcon className="h-3 w-3" />}</div>

View File

@@ -2,7 +2,7 @@ import React from "react";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// ui
import { Avatar } from "components/ui";
// helpers
@@ -71,12 +71,10 @@ export const FiltersList: React.FC<Props> = ({
}}
>
<span>
{getStateGroupIcon(
state?.group ?? "backlog",
"12",
"12",
state?.color
)}
<StateGroupIcon
stateGroup={state?.group ?? "backlog"}
color={state?.color}
/>
</span>
<span>{state?.name ?? ""}</span>
<span
@@ -105,7 +103,9 @@ export const FiltersList: React.FC<Props> = ({
backgroundColor: `${STATE_GROUP_COLORS[group]}20`,
}}
>
<span>{getStateGroupIcon(group, "16", "16")}</span>
<span>
<StateGroupIcon stateGroup={group} color={undefined} />
</span>
<span>{group}</span>
<span
className="cursor-pointer"
@@ -136,7 +136,9 @@ export const FiltersList: React.FC<Props> = ({
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>{getPriorityIcon(priority)}</span>
<span>
<PriorityIcon priority={priority} />
</span>
<span>{priority === "null" ? "None" : priority}</span>
<span
className="cursor-pointer"

View File

@@ -58,16 +58,8 @@ export const IssuesFilterView: React.FC = () => {
const isArchivedIssues = router.pathname.includes("archived-issues");
const {
issueView,
setIssueView,
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
showEmptyGroups,
showSubIssues,
setShowSubIssues,
setShowEmptyGroups,
displayFilters,
setDisplayFilters,
filters,
setFilters,
resetFilterToDefault,
@@ -96,11 +88,11 @@ export const IssuesFilterView: React.FC = () => {
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
issueView === option.type
displayFilters.layout === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setIssueView(option.type)}
onClick={() => setDisplayFilters({ layout: option.type })}
>
<option.Icon
sx={{
@@ -174,28 +166,30 @@ export const IssuesFilterView: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
GROUP_BY_OPTIONS.find((option) => option.key === groupByProperty)
?.name ?? "Select"
GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (displayFilters.layout === "kanban" && option.key === null)
return null;
if (option.key === "project") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
@@ -205,41 +199,45 @@ export const IssuesFilterView: React.FC = () => {
</div>
</div>
)}
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
groupByProperty === "priority" && option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) =>
displayFilters.group_by === "priority" &&
option.key === "priority" ? null : (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
)
)}
</CustomMenu>
</div>
</div>
</div>
)}
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters.type)
?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters.type
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -248,7 +246,7 @@ export const IssuesFilterView: React.FC = () => {
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
setDisplayFilters({
type: option.key,
})
}
@@ -260,33 +258,40 @@ export const IssuesFilterView: React.FC = () => {
</div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={showSubIssues}
onChange={() => setShowSubIssues(!showSubIssues)}
/>
</div>
</div>
)}
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<h4 className="text-custom-text-200">Show sub-issues</h4>
<div className="w-28">
<ToggleSwitch
value={showEmptyGroups}
onChange={() => setShowEmptyGroups(!showEmptyGroups)}
value={displayFilters.sub_issue ?? true}
onChange={() =>
setDisplayFilters({ sub_issue: !displayFilters.sub_issue })
}
/>
</div>
</div>
)}
{issueView !== "calendar" &&
issueView !== "spreadsheet" &&
issueView !== "gantt_chart" && (
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters.show_empty_groups,
})
}
/>
</div>
</div>
)}
{displayFilters.layout !== "calendar" &&
displayFilters.layout !== "spreadsheet" &&
displayFilters.layout !== "gantt_chart" && (
<div className="relative flex justify-end gap-x-3">
<button type="button" onClick={() => resetFilterToDefault()}>
Reset to default
@@ -302,7 +307,7 @@ export const IssuesFilterView: React.FC = () => {
)}
</div>
{issueView !== "gantt_chart" && (
{displayFilters.layout !== "gantt_chart" && (
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
@@ -310,7 +315,7 @@ export const IssuesFilterView: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
issueView === "spreadsheet" &&
displayFilters.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -318,7 +323,7 @@ export const IssuesFilterView: React.FC = () => {
return null;
if (
issueView !== "spreadsheet" &&
displayFilters.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;

View File

@@ -58,7 +58,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
);
const { setToastAlert } = useToast();
const { issueView, params } = useIssuesView();
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
@@ -126,8 +126,8 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
message: "Issues deleted successfully!",
});
if (issueView === "calendar") mutate(calendarFetchKey);
else if (issueView === "gantt_chart") mutate(ganttFetchKey);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else {
if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));

View File

@@ -15,6 +15,7 @@ import {
TAssigneesDistribution,
TCompletionChartDistribution,
TLabelsDistribution,
TStateGroups,
} from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -215,7 +216,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
<span
className="block h-3 w-3 rounded-full "
style={{
backgroundColor: STATE_GROUP_COLORS[group],
backgroundColor: STATE_GROUP_COLORS[group as TStateGroups],
}}
/>
<span className="text-xs capitalize">{group}</span>

View File

@@ -80,7 +80,7 @@ export const AllViews: React.FC<Props> = ({
const { user } = useUser();
const { memberRole } = useProjectMyMembership();
const { groupedIssues, isEmpty, issueView } = viewProps;
const { groupedIssues, isEmpty, displayFilters } = viewProps;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
@@ -117,11 +117,11 @@ export const AllViews: React.FC<Props> = ({
</StrictModeDroppable>
{groupedIssues ? (
!isEmpty ||
issueView === "kanban" ||
issueView === "calendar" ||
issueView === "gantt_chart" ? (
displayFilters?.layout === "kanban" ||
displayFilters?.layout === "calendar" ||
displayFilters?.layout === "gantt_chart" ? (
<>
{issueView === "list" ? (
{displayFilters?.layout === "list" ? (
<AllLists
states={states}
addIssueToGroup={addIssueToGroup}
@@ -134,7 +134,7 @@ export const AllViews: React.FC<Props> = ({
userAuth={memberRole}
viewProps={viewProps}
/>
) : issueView === "kanban" ? (
) : displayFilters?.layout === "kanban" ? (
<AllBoards
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
@@ -149,7 +149,7 @@ export const AllViews: React.FC<Props> = ({
userAuth={memberRole}
viewProps={viewProps}
/>
) : issueView === "calendar" ? (
) : displayFilters?.layout === "calendar" ? (
<CalendarView
handleIssueAction={handleIssueAction}
addIssueToDate={addIssueToDate}
@@ -157,7 +157,7 @@ export const AllViews: React.FC<Props> = ({
user={user}
userAuth={memberRole}
/>
) : issueView === "spreadsheet" ? (
) : displayFilters?.layout === "spreadsheet" ? (
<SpreadsheetView
handleIssueAction={handleIssueAction}
openIssuesListModal={cycleId || moduleId ? openIssuesListModal : null}
@@ -166,7 +166,7 @@ export const AllViews: React.FC<Props> = ({
userAuth={memberRole}
/>
) : (
issueView === "gantt_chart" && <GanttChartView />
displayFilters?.layout === "gantt_chart" && <GanttChartView />
)}
</>
) : router.pathname.includes("archived-issues") ? (

View File

@@ -1,7 +1,7 @@
// components
import { SingleBoard } from "components/core/views/board-view/single-board";
// icons
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// types
@@ -36,7 +36,7 @@ export const AllBoards: React.FC<Props> = ({
userAuth,
viewProps,
}) => {
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
const { displayFilters, groupedIssues } = viewProps;
return (
<>
@@ -44,9 +44,12 @@ export const AllBoards: React.FC<Props> = ({
<div className="horizontal-scroll-enable flex h-full gap-x-4 p-8">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
return (
<SingleBoard
@@ -67,13 +70,15 @@ export const AllBoards: React.FC<Props> = ({
/>
);
})}
{!showEmptyGroups && (
{!displayFilters?.show_empty_groups && (
<div className="h-full w-96 flex-shrink-0 space-y-2 p-1">
<h2 className="text-lg font-semibold">Hidden groups</h2>
<div className="space-y-3">
{Object.keys(groupedIssues).map((singleGroup, index) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (groupedIssues[singleGroup].length === 0)
return (
@@ -82,10 +87,16 @@ export const AllBoards: React.FC<Props> = ({
className="flex items-center justify-between gap-2 rounded bg-custom-background-90 p-2 shadow"
>
<div className="flex items-center gap-2">
{currentState &&
getStateGroupIcon(currentState.group, "16", "16", currentState.color)}
{currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
)}
<h4 className="text-sm capitalize">
{selectedGroup === "state"
{displayFilters?.group_by === "state"
? addSpaceIfCamelCase(currentState?.name ?? "")
: addSpaceIfCamelCase(singleGroup)}
</h4>

View File

@@ -13,14 +13,16 @@ import useProjects from "hooks/use-projects";
import { Avatar, Icon } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
// types
import { IIssueViewProps, IState } from "types";
import { IIssueViewProps, IState, TIssuePriorities, TStateGroups } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
@@ -46,22 +48,28 @@ export const BoardHeader: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { groupedIssues, groupByProperty: selectedGroup } = viewProps;
const { displayFilters, groupedIssues } = viewProps;
console.log("dF", displayFilters);
const { data: issueLabels } = useSWR(
workspaceSlug && projectId && selectedGroup === "labels"
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? PROJECT_ISSUE_LABELS(projectId.toString())
: null,
workspaceSlug && projectId && selectedGroup === "labels"
workspaceSlug && projectId && displayFilters?.group_by === "labels"
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
: null
);
const { data: members } = useSWR(
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? PROJECT_MEMBERS(projectId.toString())
: null,
workspaceSlug && projectId && (selectedGroup === "created_by" || selectedGroup === "assignees")
workspaceSlug &&
projectId &&
(displayFilters?.group_by === "created_by" || displayFilters?.group_by === "assignees")
? () => projectService.projectMembers(workspaceSlug.toString(), projectId.toString())
: null
);
@@ -71,7 +79,7 @@ export const BoardHeader: React.FC<Props> = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (selectedGroup) {
switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -95,16 +103,29 @@ export const BoardHeader: React.FC<Props> = ({
const getGroupIcon = () => {
let icon;
switch (selectedGroup) {
switch (displayFilters?.group_by) {
case "state":
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
icon = currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break;
case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16");
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "text-lg");
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
@@ -152,7 +173,7 @@ export const BoardHeader: React.FC<Props> = ({
<span className="flex items-center">{getGroupIcon()}</span>
<h2
className={`text-lg font-semibold truncate ${
selectedGroup === "created_by" ? "" : "capitalize"
displayFilters?.group_by === "created_by" ? "" : "capitalize"
}`}
style={{
writingMode: isCollapsed ? "horizontal-tb" : "vertical-rl",
@@ -183,7 +204,7 @@ export const BoardHeader: React.FC<Props> = ({
<Icon iconName="open_in_full" className="text-base font-medium text-custom-text-900" />
)}
</button>
{!disableAddIssue && !disableUserActions && selectedGroup !== "created_by" && (
{!disableAddIssue && !disableUserActions && displayFilters?.group_by !== "created_by" && (
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 text-custom-text-200 outline-none duration-300 hover:bg-custom-background-80"

View File

@@ -50,7 +50,7 @@ export const SingleBoard: React.FC<Props> = ({
// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);
const { groupedIssues, groupByProperty: selectedGroup, orderBy, properties } = viewProps;
const { displayFilters, groupedIssues, properties } = viewProps;
const router = useRouter();
const { cycleId, moduleId } = router.query;
@@ -80,14 +80,14 @@ export const SingleBoard: React.FC<Props> = ({
{(provided, snapshot) => (
<div
className={`relative h-full ${
orderBy !== "sort_order" && snapshot.isDraggingOver
displayFilters?.order_by !== "sort_order" && snapshot.isDraggingOver
? "bg-custom-background-100/20"
: ""
} ${!isCollapsed ? "hidden" : "flex flex-col"}`}
ref={provided.innerRef}
{...provided.droppableProps}
>
{orderBy !== "sort_order" && (
{displayFilters?.order_by !== "sort_order" && (
<>
<div
className={`absolute ${
@@ -101,7 +101,11 @@ export const SingleBoard: React.FC<Props> = ({
>
This board is ordered by{" "}
{replaceUnderscoreIfSnakeCase(
orderBy ? (orderBy[0] === "-" ? orderBy.slice(1) : orderBy) : "created_at"
displayFilters?.order_by
? displayFilters?.order_by[0] === "-"
? displayFilters?.order_by.slice(1)
: displayFilters?.order_by
: "created_at"
)}
</div>
</>
@@ -145,13 +149,13 @@ export const SingleBoard: React.FC<Props> = ({
))}
<span
style={{
display: orderBy === "sort_order" ? "inline" : "none",
display: displayFilters?.order_by === "sort_order" ? "inline" : "none",
}}
>
{provided.placeholder}
</span>
</div>
{selectedGroup !== "created_by" && (
{displayFilters?.group_by !== "created_by" && (
<div>
{type === "issue"
? !disableAddIssueOption && (

View File

@@ -93,7 +93,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
const actionSectionRef = useRef<HTMLDivElement | null>(null);
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
const { displayFilters, properties, mutateIssues } = viewProps;
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -131,9 +131,9 @@ export const SingleBoardIssue: React.FC<Props> = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
displayFilters?.group_by ?? null,
index,
orderBy,
displayFilters?.order_by ?? "-created_at",
prevData
),
false
@@ -149,24 +149,14 @@ export const SingleBoardIssue: React.FC<Props> = ({
if (moduleId) mutate(MODULE_DETAILS(moduleId as string));
});
},
[
workspaceSlug,
cycleId,
moduleId,
groupTitle,
index,
selectedGroup,
mutateIssues,
orderBy,
user,
]
[displayFilters, workspaceSlug, cycleId, moduleId, groupTitle, index, mutateIssues, user]
);
const getStyle = (
style: DraggingStyle | NotDraggingStyle | undefined,
snapshot: DraggableStateSnapshot
) => {
if (orderBy === "sort_order") return style;
if (displayFilters?.order_by === "sort_order") return style;
if (!snapshot.isDragging) return {};
if (!snapshot.isDropAnimating) return style;

View File

@@ -60,7 +60,7 @@ export const CalendarView: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { calendarIssues, params, setCalendarDateRange } = useCalendarIssuesView();
const { calendarIssues, params, setDisplayFilters } = useCalendarIssuesView();
const totalDate = eachDayOfInterval({
start: calendarDates.startDate,
@@ -152,18 +152,20 @@ export const CalendarView: React.FC<Props> = ({
endDate,
});
setCalendarDateRange(
`${renderDateFormat(startDate)};after,${renderDateFormat(endDate)};before`
);
setDisplayFilters({
calendar_date_range: `${renderDateFormat(startDate)};after,${renderDateFormat(
endDate
)};before`,
});
};
useEffect(() => {
setCalendarDateRange(
`${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
setDisplayFilters({
calendar_date_range: `${renderDateFormat(startOfWeek(currentDate))};after,${renderDateFormat(
lastDayOfWeek(currentDate)
)};before`
);
}, [currentDate]);
)};before`,
});
}, [currentDate, setDisplayFilters]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;

View File

@@ -29,7 +29,7 @@ import { PlusIcon } from "@heroicons/react/24/outline";
import { getStatesList } from "helpers/state.helper";
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue, IIssueFilterOptions, IState } from "types";
import { IIssue, IIssueFilterOptions, IState, TIssuePriorities } from "types";
// fetch-keys
import {
CYCLE_DETAILS,
@@ -77,18 +77,8 @@ export const IssuesView: React.FC<Props> = ({
const { setToastAlert } = useToast();
const {
groupedByIssues,
mutateIssues,
issueView,
groupByProperty: selectedGroup,
orderBy,
filters,
isEmpty,
setFilters,
params,
showEmptyGroups,
} = useIssuesView();
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
useIssuesView();
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const { data: stateGroups } = useSWR(
@@ -129,7 +119,7 @@ export const IssuesView: React.FC<Props> = ({
if (destination.droppableId === "trashBox") {
handleDeleteIssue(draggedItem);
} else {
if (orderBy === "sort_order") {
if (displayFilters.order_by === "sort_order") {
let newSortOrder = draggedItem.sort_order;
const destinationGroupArray = groupedByIssues[destination.droppableId];
@@ -177,15 +167,19 @@ export const IssuesView: React.FC<Props> = ({
const destinationGroup = destination.droppableId; // destination group id
if (orderBy === "sort_order" || source.droppableId !== destination.droppableId) {
if (
displayFilters.order_by === "sort_order" ||
source.droppableId !== destination.droppableId
) {
// different group/column;
// source.droppableId !== destination.droppableId -> even if order by is not sort_order,
// if the issue is moved to a different group, then we will change the group of the
// dragged item(or issue)
if (selectedGroup === "priority") draggedItem.priority = destinationGroup;
else if (selectedGroup === "state") {
if (displayFilters.group_by === "priority")
draggedItem.priority = destinationGroup as TIssuePriorities;
else if (displayFilters.group_by === "state") {
draggedItem.state = destinationGroup;
draggedItem.state_detail = states?.find((s) => s.id === destinationGroup) as IState;
}
@@ -212,8 +206,14 @@ export const IssuesView: React.FC<Props> = ({
return {
...prevData,
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
[sourceGroup]: orderArrayBy(
sourceGroupArray,
displayFilters.order_by ?? "-created_at"
),
[destinationGroup]: orderArrayBy(
destinationGroupArray,
displayFilters.order_by ?? "-created_at"
),
};
},
false
@@ -266,13 +266,13 @@ export const IssuesView: React.FC<Props> = ({
}
},
[
displayFilters.group_by,
displayFilters.order_by,
workspaceSlug,
cycleId,
moduleId,
groupedByIssues,
projectId,
selectedGroup,
orderBy,
handleDeleteIssue,
params,
states,
@@ -286,19 +286,19 @@ export const IssuesView: React.FC<Props> = ({
let preloadedValue: string | string[] = groupTitle;
if (selectedGroup === "labels") {
if (displayFilters.group_by === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (selectedGroup)
if (displayFilters.group_by)
setPreloadedData({
[selectedGroup]: preloadedValue,
[displayFilters.group_by]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData, selectedGroup]
[displayFilters.group_by, setCreateIssueModal, setPreloadedData]
);
const addIssueToDate = useCallback(
@@ -351,7 +351,7 @@ export const IssuesView: React.FC<Props> = ({
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
if (displayFilters.group_by) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -383,7 +383,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[workspaceSlug, projectId, cycleId, params, selectedGroup, setToastAlert]
[displayFilters.group_by, workspaceSlug, projectId, cycleId, params, setToastAlert]
);
const removeIssueFromModule = useCallback(
@@ -394,7 +394,7 @@ export const IssuesView: React.FC<Props> = ({
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData: any) => {
if (!prevData) return prevData;
if (selectedGroup) {
if (displayFilters.group_by) {
const filteredData: any = {};
for (const key in prevData) {
filteredData[key] = prevData[key].filter((item: any) => item.id !== issueId);
@@ -426,7 +426,7 @@ export const IssuesView: React.FC<Props> = ({
console.log(e);
});
},
[workspaceSlug, projectId, moduleId, params, selectedGroup, setToastAlert]
[displayFilters.group_by, workspaceSlug, projectId, moduleId, params, setToastAlert]
);
const nullFilters = Object.keys(filters).filter(
@@ -480,7 +480,6 @@ export const IssuesView: React.FC<Props> = ({
state: null,
start_date: null,
target_date: null,
type: null,
})
}
/>
@@ -512,10 +511,10 @@ export const IssuesView: React.FC<Props> = ({
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={
selectedGroup === "created_by" ||
selectedGroup === "labels" ||
selectedGroup === "state_detail.group" ||
selectedGroup === "assignees"
displayFilters.group_by === "created_by" ||
displayFilters.group_by === "labels" ||
displayFilters.group_by === "state_detail.group" ||
displayFilters.group_by === "assignees"
}
emptyState={{
title: cycleId
@@ -553,15 +552,12 @@ export const IssuesView: React.FC<Props> = ({
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
groupByProperty: selectedGroup,
groupedIssues: groupedByIssues,
displayFilters,
isEmpty,
issueView,
mutateIssues,
orderBy,
params,
properties,
showEmptyGroups,
}}
/>
</>

View File

@@ -29,7 +29,7 @@ export const AllLists: React.FC<Props> = ({
userAuth,
viewProps,
}) => {
const { groupByProperty: selectedGroup, groupedIssues, showEmptyGroups } = viewProps;
const { displayFilters, groupedIssues } = viewProps;
return (
<>
@@ -37,9 +37,12 @@ export const AllLists: React.FC<Props> = ({
<div className="h-full overflow-y-auto">
{Object.keys(groupedIssues).map((singleGroup) => {
const currentState =
selectedGroup === "state" ? states?.find((s) => s.id === singleGroup) : null;
displayFilters?.group_by === "state"
? states?.find((s) => s.id === singleGroup)
: null;
if (!showEmptyGroups && groupedIssues[singleGroup].length === 0) return null;
if (!displayFilters?.show_empty_groups && groupedIssues[singleGroup].length === 0)
return null;
return (
<SingleList

View File

@@ -91,7 +91,7 @@ export const SingleListIssue: React.FC<Props> = ({
const { setToastAlert } = useToast();
const { groupByProperty: selectedGroup, orderBy, properties, mutateIssues } = viewProps;
const { displayFilters, properties, mutateIssues } = viewProps;
const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issue: IIssue) => {
@@ -124,9 +124,9 @@ export const SingleListIssue: React.FC<Props> = ({
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
displayFilters?.group_by ?? null,
index,
orderBy,
displayFilters?.order_by ?? "-created_at",
prevData
),
false
@@ -148,15 +148,14 @@ export const SingleListIssue: React.FC<Props> = ({
});
},
[
displayFilters,
workspaceSlug,
cycleId,
moduleId,
userId,
groupTitle,
index,
selectedGroup,
mutateIssues,
orderBy,
user,
]
);

View File

@@ -15,7 +15,7 @@ import { SingleListIssue } from "components/core";
import { Avatar, CustomMenu } from "components/ui";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { renderEmoji } from "helpers/emoji.helper";
@@ -26,10 +26,14 @@ import {
IIssueLabels,
IIssueViewProps,
IState,
TIssuePriorities,
TStateGroups,
UserAuth,
} from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS, PROJECT_MEMBERS } from "constants/fetch-keys";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
currentState?: IState | null;
@@ -65,7 +69,7 @@ export const SingleList: React.FC<Props> = ({
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const { groupByProperty: selectedGroup, groupedIssues } = viewProps;
const { displayFilters, groupedIssues } = viewProps;
const { data: issueLabels } = useSWR<IIssueLabels[]>(
workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null,
@@ -86,7 +90,7 @@ export const SingleList: React.FC<Props> = ({
const getGroupTitle = () => {
let title = addSpaceIfCamelCase(groupTitle);
switch (selectedGroup) {
switch (displayFilters?.group_by) {
case "state":
title = addSpaceIfCamelCase(currentState?.name ?? "");
break;
@@ -109,16 +113,29 @@ export const SingleList: React.FC<Props> = ({
const getGroupIcon = () => {
let icon;
switch (selectedGroup) {
switch (displayFilters?.group_by) {
case "state":
icon =
currentState && getStateGroupIcon(currentState.group, "16", "16", currentState.color);
icon = currentState && (
<StateGroupIcon
stateGroup={currentState.group}
color={currentState.color}
height="16px"
width="16px"
/>
);
break;
case "state_detail.group":
icon = getStateGroupIcon(groupTitle as any, "16", "16");
icon = (
<StateGroupIcon
stateGroup={groupTitle as TStateGroups}
color={STATE_GROUP_COLORS[groupTitle as TStateGroups]}
height="16px"
width="16px"
/>
);
break;
case "priority":
icon = getPriorityIcon(groupTitle, "text-lg");
icon = <PriorityIcon priority={groupTitle as TIssuePriorities} className="text-lg" />;
break;
case "project":
const project = projects?.find((p) => p.id === groupTitle);
@@ -160,13 +177,13 @@ export const SingleList: React.FC<Props> = ({
<div className="flex items-center justify-between px-4 py-2.5 bg-custom-background-90">
<Disclosure.Button>
<div className="flex items-center gap-x-3">
{selectedGroup !== null && (
{displayFilters?.group_by !== null && (
<div className="flex items-center">{getGroupIcon()}</div>
)}
{selectedGroup !== null ? (
{displayFilters?.group_by !== null ? (
<h2
className={`text-sm font-semibold leading-6 text-custom-text-100 ${
selectedGroup === "created_by" ? "" : "capitalize"
displayFilters?.group_by === "created_by" ? "" : "capitalize"
}`}
>
{getGroupTitle()}

View File

@@ -22,10 +22,10 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
const { storedValue: activeSortingProperty, setValue: setActiveSortingProperty } =
useLocalStorage("spreadsheetViewActiveSortingProperty", "");
const { orderBy, setOrderBy } = useSpreadsheetIssuesView();
const { displayFilters, setDisplayFilters } = useSpreadsheetIssuesView();
const handleOrderBy = (order: TIssueOrderByOptions, itemKey: string) => {
setOrderBy(order);
setDisplayFilters({ order_by: order });
setSelectedMenuItem(`${order}_${itemKey}`);
setActiveSortingProperty(order === "-created_at" ? "" : itemKey);
};
@@ -239,7 +239,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
orderBy !== "-created_at" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(col.propertyName) && (
<CustomMenu.MenuItem
className={`mt-0.5${

View File

@@ -19,7 +19,7 @@ import { ActiveCycleProgressStats } from "components/cycles";
// icons
import { CalendarDaysIcon } from "@heroicons/react/20/solid";
import { getPriorityIcon } from "components/icons/priority-icon";
import { PriorityIcon } from "components/icons/priority-icon";
import {
TargetIcon,
ContrastIcon,
@@ -28,7 +28,7 @@ import {
TriangleExclamationIcon,
AlarmClockIcon,
LayerDiagonalIcon,
CompletedStateIcon,
StateGroupIcon,
} from "components/icons";
import { StarIcon } from "@heroicons/react/24/outline";
// components
@@ -385,8 +385,8 @@ export const ActiveCycleDetails: React.FC = () => {
<LayerDiagonalIcon className="h-4 w-4 flex-shrink-0" />
{cycle.total_issues} issues
</div>
<div className="flex gap-2">
<CompletedStateIcon width={16} height={16} color="#438AF3" />
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup="completed" height="14px" width="14px" />
{cycle.completed_issues} issues
</div>
</div>
@@ -477,7 +477,7 @@ export const ActiveCycleDetails: React.FC = () => {
: "border-orange-500/20 bg-orange-500/20 text-orange-500"
}`}
>
{getPriorityIcon(issue.priority, "text-sm")}
<PriorityIcon priority={issue.priority} className="text-sm" />
</div>
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
<div className={`flex items-center gap-2 text-custom-text-200`}>

View File

@@ -16,7 +16,7 @@ export const CycleIssuesGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const { orderBy } = useIssuesView();
const { displayFilters } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
@@ -44,7 +44,7 @@ export const CycleIssuesGanttChartView = () => {
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={orderBy === "sort_order" && isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>

View File

@@ -1,21 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const BacklogStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "rgb(var(--color-text-200))",
}) => (
<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,78 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const CancelledStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f2655a",
}) => (
<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,69 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const CompletedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#438af3",
}) => (
<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 +1,7 @@
export * from "./module";
export * from "./state";
export * from "./alarm-clock-icon";
export * from "./attachment-icon";
export * from "./backlog-state-icon";
export * from "./blocked-icon";
export * from "./blocker-icon";
export * from "./bolt-icon";
@@ -8,12 +9,10 @@ export * from "./calendar-before-icon";
export * from "./calendar-after-icon";
export * from "./calendar-month-icon";
export * from "./cancel-icon";
export * from "./cancelled-state-icon";
export * from "./clipboard-icon";
export * from "./color-pallette-icon";
export * from "./comment-icon";
export * from "./completed-cycle-icon";
export * from "./completed-state-icon";
export * from "./current-cycle-icon";
export * from "./cycle-icon";
export * from "./discord-icon";
@@ -23,11 +22,9 @@ export * from "./ellipsis-horizontal-icon";
export * from "./external-link-icon";
export * from "./github-icon";
export * from "./heartbeat-icon";
export * from "./started-state-icon";
export * from "./layer-diagonal-icon";
export * from "./lock-icon";
export * from "./menu-icon";
export * from "./module";
export * from "./pencil-scribble-icon";
export * from "./plus-icon";
export * from "./person-running-icon";
@@ -36,11 +33,8 @@ export * from "./question-mark-circle-icon";
export * from "./setting-icon";
export * from "./signal-cellular-icon";
export * from "./stacked-layers-icon";
export * from "./started-state-icon";
export * from "./state-group-icon";
export * from "./tag-icon";
export * from "./tune-icon";
export * from "./unstarted-state-icon";
export * from "./upcoming-cycle-icon";
export * from "./user-group-icon";
export * from "./user-icon-circle";

View File

@@ -1,22 +1,25 @@
export const getPriorityIcon = (priority: string | null, className?: string) => {
// types
import { TIssuePriorities } from "types";
type Props = {
priority: TIssuePriorities | null;
className?: string;
};
export const PriorityIcon: React.FC<Props> = ({ priority, className = "" }) => {
if (!className || className === "") className = "text-xs flex items-center";
priority = priority?.toLowerCase() ?? null;
switch (priority) {
case "urgent":
return <span className={`material-symbols-rounded ${className}`}>error</span>;
case "high":
return <span className={`material-symbols-rounded ${className}`}>signal_cellular_alt</span>;
case "medium":
return (
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_2_bar</span>
);
case "low":
return (
<span className={`material-symbols-rounded ${className}`}>signal_cellular_alt_1_bar</span>
);
default:
return <span className={`material-symbols-rounded ${className}`}>block</span>;
}
return (
<span className={`material-symbols-rounded ${className}`}>
{priority === "urgent"
? "error"
: priority === "high"
? "signal_cellular_alt"
: priority === "medium"
? "signal_cellular_alt_2_bar"
: priority === "low"
? "signal_cellular_alt_1_bar"
: "block"}
</span>
);
};

View File

@@ -1,77 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const StartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#fbb040",
}) => (
<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,66 +0,0 @@
import {
BacklogStateIcon,
CancelledStateIcon,
CompletedStateIcon,
StartedStateIcon,
UnstartedStateIcon,
} from "components/icons";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
export const getStateGroupIcon = (
stateGroup: "backlog" | "unstarted" | "started" | "completed" | "cancelled",
width = "20",
height = "20",
color?: string
) => {
switch (stateGroup) {
case "backlog":
return (
<BacklogStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
className="flex-shrink-0"
/>
);
case "unstarted":
return (
<UnstartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
className="flex-shrink-0"
/>
);
case "started":
return (
<StartedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
className="flex-shrink-0"
/>
);
case "completed":
return (
<CompletedStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
className="flex-shrink-0"
/>
);
case "cancelled":
return (
<CancelledStateIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
className="flex-shrink-0"
/>
);
default:
return <></>;
}
};

View File

@@ -0,0 +1,24 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupBacklogIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#a3a3a3",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" stroke-dasharray="4 4" />
</svg>
);

View File

@@ -0,0 +1,34 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupCancelledIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#ef4444",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4052_100277)">
<path
d="M8 8.84L10.58 11.42C10.7 11.54 10.84 11.6 11 11.6C11.16 11.6 11.3 11.54 11.42 11.42C11.54 11.3 11.6 11.16 11.6 11C11.6 10.84 11.54 10.7 11.42 10.58L8.84 8L11.42 5.42C11.54 5.3 11.6 5.16 11.6 5C11.6 4.84 11.54 4.7 11.42 4.58C11.3 4.46 11.16 4.4 11 4.4C10.84 4.4 10.7 4.46 10.58 4.58L8 7.16L5.42 4.58C5.3 4.46 5.16 4.4 5 4.4C4.84 4.4 4.7 4.46 4.58 4.58C4.46 4.7 4.4 4.84 4.4 5C4.4 5.16 4.46 5.3 4.58 5.42L7.16 8L4.58 10.58C4.46 10.7 4.4 10.84 4.4 11C4.4 11.16 4.46 11.3 4.58 11.42C4.7 11.54 4.84 11.6 5 11.6C5.16 11.6 5.3 11.54 5.42 11.42L8 8.84ZM8 16C6.90667 16 5.87333 15.79 4.9 15.37C3.92667 14.95 3.07667 14.3767 2.35 13.65C1.62333 12.9233 1.05 12.0733 0.63 11.1C0.21 10.1267 0 9.09333 0 8C0 6.89333 0.21 5.85333 0.63 4.88C1.05 3.90667 1.62333 3.06 2.35 2.34C3.07667 1.62 3.92667 1.05 4.9 0.63C5.87333 0.21 6.90667 0 8 0C9.10667 0 10.1467 0.21 11.12 0.63C12.0933 1.05 12.94 1.62 13.66 2.34C14.38 3.06 14.95 3.90667 15.37 4.88C15.79 5.85333 16 6.89333 16 8C16 9.09333 15.79 10.1267 15.37 11.1C14.95 12.0733 14.38 12.9233 13.66 13.65C12.94 14.3767 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM8 14.8C9.89333 14.8 11.5 14.1367 12.82 12.81C14.14 11.4833 14.8 9.88 14.8 8C14.8 6.10667 14.14 4.5 12.82 3.18C11.5 1.86 9.89333 1.2 8 1.2C6.12 1.2 4.51667 1.86 3.19 3.18C1.86333 4.5 1.2 6.10667 1.2 8C1.2 9.88 1.86333 11.4833 3.19 12.81C4.51667 14.1367 6.12 14.8 8 14.8Z"
fill={color}
/>
</g>
<defs>
<clipPath id="clip0_4052_100277">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);

View File

@@ -0,0 +1,27 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupCompletedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#16a34a",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.80486 9.80731L4.84856 7.85103C4.73197 7.73443 4.58542 7.67478 4.4089 7.67208C4.23238 7.66937 4.08312 7.72902 3.96113 7.85103C3.83913 7.97302 3.77814 8.12093 3.77814 8.29474C3.77814 8.46855 3.83913 8.61645 3.96113 8.73844L6.27206 11.0494C6.42428 11.2016 6.60188 11.2777 6.80486 11.2777C7.00782 11.2777 7.18541 11.2016 7.33764 11.0494L12.0227 6.36435C12.1393 6.24776 12.1989 6.10121 12.2016 5.92469C12.2043 5.74817 12.1447 5.59891 12.0227 5.47692C11.9007 5.35493 11.7528 5.29393 11.579 5.29393C11.4051 5.29393 11.2572 5.35493 11.1353 5.47692L6.80486 9.80731ZM8.00141 16C6.89494 16 5.85491 15.79 4.88132 15.3701C3.90772 14.9502 3.06082 14.3803 2.34064 13.6604C1.62044 12.9405 1.05028 12.094 0.63017 11.1208C0.210057 10.1477 0 9.10788 0 8.00141C0 6.89494 0.209966 5.85491 0.629896 4.88132C1.04983 3.90772 1.61972 3.06082 2.33958 2.34064C3.05946 1.62044 3.90598 1.05028 4.87915 0.630171C5.8523 0.210058 6.89212 0 7.99859 0C9.10506 0 10.1451 0.209966 11.1187 0.629897C12.0923 1.04983 12.9392 1.61972 13.6594 2.33959C14.3796 3.05946 14.9497 3.90598 15.3698 4.87915C15.7899 5.8523 16 6.89212 16 7.99859C16 9.10506 15.79 10.1451 15.3701 11.1187C14.9502 12.0923 14.3803 12.9392 13.6604 13.6594C12.9405 14.3796 12.094 14.9497 11.1208 15.3698C10.1477 15.7899 9.10788 16 8.00141 16ZM8 14.7369C9.88071 14.7369 11.4737 14.0842 12.779 12.779C14.0842 11.4737 14.7369 9.88071 14.7369 8C14.7369 6.11929 14.0842 4.52631 12.779 3.22104C11.4737 1.91577 9.88071 1.26314 8 1.26314C6.11929 1.26314 4.52631 1.91577 3.22104 3.22104C1.91577 4.52631 1.26314 6.11929 1.26314 8C1.26314 9.88071 1.91577 11.4737 3.22104 12.779C4.52631 14.0842 6.11929 14.7369 8 14.7369Z"
fill={color}
/>
</svg>
);

View File

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

View File

@@ -0,0 +1,25 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupStartedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#f59e0b",
}) => (
<svg
height={height}
width={width}
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12"
fill="none"
>
<circle cx="6" cy="6" r="5.6" stroke={color} stroke-width="0.8" />
<circle cx="6" cy="6" r="3.35" stroke={color} stroke-width="0.8" stroke-dasharray="2.4 2.4" />
</svg>
);

View File

@@ -0,0 +1,74 @@
// icons
import {
StateGroupBacklogIcon,
StateGroupCancelledIcon,
StateGroupCompletedIcon,
StateGroupStartedIcon,
StateGroupUnstartedIcon,
} from "components/icons";
// types
import { TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
type Props = {
className?: string;
color?: string;
height?: string;
stateGroup: TStateGroups;
width?: string;
};
export const StateGroupIcon: React.FC<Props> = ({
className = "",
color,
height = "12px",
width = "12px",
stateGroup,
}) => {
if (stateGroup === "backlog")
return (
<StateGroupBacklogIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["backlog"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "cancelled")
return (
<StateGroupCancelledIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["cancelled"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "completed")
return (
<StateGroupCompletedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["completed"]}
className={`flex-shrink-0 ${className}`}
/>
);
else if (stateGroup === "started")
return (
<StateGroupStartedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["started"]}
className={`flex-shrink-0 ${className}`}
/>
);
else
return (
<StateGroupUnstartedIcon
width={width}
height={height}
color={color ?? STATE_GROUP_COLORS["unstarted"]}
className={`flex-shrink-0 ${className}`}
/>
);
};

View File

@@ -0,0 +1,24 @@
type Props = {
width?: string;
height?: string;
className?: string;
color?: string;
};
export const StateGroupUnstartedIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "#3a3a3a",
}) => (
<svg
height={height}
width={width}
className={className}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
</svg>
);

View File

@@ -1,59 +0,0 @@
import React from "react";
import type { Props } from "./types";
export const UnstartedStateIcon: React.FC<Props> = ({
width = "20",
height = "20",
className,
color = "rgb(var(--color-text-200))",
}) => (
<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

@@ -3,7 +3,7 @@ import useInboxView from "hooks/use-inbox-view";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons";
import { PriorityIcon } from "components/icons";
// constants
import { PRIORITIES } from "constants/project";
import { INBOX_STATUS } from "constants/inbox";
@@ -42,7 +42,7 @@ export const FiltersDropdown: React.FC = () => {
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"}
<PriorityIcon priority={priority} /> {priority ?? "None"}
</div>
),
value: {

View File

@@ -2,9 +2,11 @@
import useInboxView from "hooks/use-inbox-view";
// icons
import { XMarkIcon } from "@heroicons/react/24/outline";
import { getPriorityIcon } from "components/icons";
import { PriorityIcon } from "components/icons";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { TIssuePriorities } from "types";
// constants
import { INBOX_STATUS } from "constants/inbox";
@@ -48,7 +50,9 @@ export const InboxFiltersList = () => {
: "bg-custom-background-90 text-custom-text-200"
}`}
>
<span>{getPriorityIcon(priority)}</span>
<span>
<PriorityIcon priority={priority as TIssuePriorities} />
</span>
<button
type="button"
className="cursor-pointer"

View File

@@ -4,7 +4,7 @@ import Link from "next/link";
// ui
import { Tooltip } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons";
import { PriorityIcon } from "components/icons";
import {
CalendarDaysIcon,
CheckCircleIcon,
@@ -65,10 +65,7 @@ export const InboxIssueCard: React.FC<Props> = (props) => {
: "border-custom-border-200"
}`}
>
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
"text-sm"
)}
<PriorityIcon priority={issue.priority ?? null} className="text-sm" />
</div>
</Tooltip>
<Tooltip

View File

@@ -35,7 +35,7 @@ import type { IInboxIssue, IIssue } from "types";
// fetch-keys
import { INBOX_ISSUES, INBOX_ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
const defaultValues = {
const defaultValues: Partial<IInboxIssue> = {
name: "",
description_html: "",
estimate_point: null,

View File

@@ -50,7 +50,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
const { workspaceSlug, projectId, cycleId, moduleId, viewId, issueId } = router.query;
const isArchivedIssues = router.pathname.includes("archived-issues");
const { issueView, params } = useIssuesView();
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
@@ -73,7 +73,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
await issueServices
.deleteIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => {
if (issueView === "calendar") {
if (displayFilters.layout === "calendar") {
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
@@ -87,7 +87,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
(prevData) => (prevData ?? []).filter((p) => p.id !== data.id),
false
);
} else if (issueView === "spreadsheet") {
} else if (displayFilters.layout === "spreadsheet") {
const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
: moduleId

View File

@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
// ui
import { Tooltip } from "components/ui";
// icons
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { findTotalDaysInRange, renderShortDate } from "helpers/date-time.helper";
// types
@@ -52,7 +52,7 @@ export const IssueGanttSidebarBlock = ({ data }: { data: IIssue }) => {
className="relative w-full flex items-center gap-2 h-full cursor-pointer"
onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/issues/${data?.id}`)}
>
{getStateGroupIcon(data?.state_detail?.group, "14", "14", data?.state_detail?.color)}
<StateGroupIcon stateGroup={data?.state_detail?.group} color={data?.state_detail?.color} />
<div className="text-xs text-custom-text-300 flex-shrink-0">
{data?.project_detail?.identifier} {data?.sequence_id}
</div>

View File

@@ -16,7 +16,7 @@ export const IssueGanttChartView = () => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { orderBy } = useIssuesView();
const { displayFilters } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
@@ -43,7 +43,7 @@ export const IssueGanttChartView = () => {
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={orderBy === "sort_order" && isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>

View File

@@ -78,7 +78,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
const { issueView, params } = useIssuesView();
const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView();
@@ -247,13 +247,13 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.module && payload.module !== "")
await addIssueToModule(res.id, payload.module);
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "gantt_chart")
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (groupedIssues) mutateMyIssues();
setToastAlert({
@@ -285,8 +285,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else {
if (issueView === "calendar") mutate(calendarFetchKey);
if (issueView === "spreadsheet") mutate(spreadsheetFetchKey);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}

View File

@@ -11,11 +11,11 @@ import { DateFilterModal } from "components/core";
// ui
import { MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
// types
import { IIssueFilterOptions, IQuery } from "types";
import { IIssueFilterOptions, IQuery, TStateGroups } from "types";
// fetch-keys
import { WORKSPACE_LABELS } from "constants/fetch-keys";
// constants
@@ -83,7 +83,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"}
<PriorityIcon priority={priority} /> {priority ?? "None"}
</div>
),
value: {
@@ -104,7 +104,7 @@ export const MyIssuesSelectFilters: React.FC<Props> = ({
id: key,
label: (
<div className="flex items-center gap-2">
{getStateGroupIcon(key as any, "16", "16")}{" "}
<StateGroupIcon stateGroup={key as TStateGroups} />
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
</div>
),

View File

@@ -37,20 +37,8 @@ export const MyIssuesViewOptions: React.FC = () => {
const router = useRouter();
const { workspaceSlug } = router.query;
const {
issueView,
setIssueView,
groupBy,
setGroupBy,
orderBy,
setOrderBy,
showEmptyGroups,
setShowEmptyGroups,
properties,
setProperty,
filters,
setFilters,
} = useMyIssuesFilters(workspaceSlug?.toString());
const { displayFilters, setDisplayFilters, properties, setProperty, filters, setFilters } =
useMyIssuesFilters(workspaceSlug?.toString());
const { isEstimateActive } = useEstimateOption();
@@ -68,11 +56,11 @@ export const MyIssuesViewOptions: React.FC = () => {
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
issueView === option.type
displayFilters?.layout === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setIssueView(option.type)}
onClick={() => setDisplayFilters({ layout: option.type })}
>
<option.Icon
sx={{
@@ -139,81 +127,89 @@ export const MyIssuesViewOptions: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
groupBy === "project"
? "Project"
: GROUP_BY_OPTIONS.find((option) => option.key === groupBy)
?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupBy(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (groupBy === "priority" && option.key === "priority")
return null;
if (option.key === "sort_order") return null;
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
</div>
</>
)}
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -222,7 +218,7 @@ export const MyIssuesViewOptions: React.FC = () => {
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
setDisplayFilters({
type: option.key,
})
}
@@ -234,16 +230,24 @@ export const MyIssuesViewOptions: React.FC = () => {
</div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
</div>
</div>
</>
)}
</>
)}
</div>
<div className="space-y-2 py-3">
@@ -253,7 +257,7 @@ export const MyIssuesViewOptions: React.FC = () => {
if (key === "estimate" && !isEstimateActive) return null;
if (
issueView === "spreadsheet" &&
displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -261,7 +265,7 @@ export const MyIssuesViewOptions: React.FC = () => {
return null;
if (
issueView !== "spreadsheet" &&
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;

View File

@@ -18,7 +18,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue, IIssueFilterOptions } from "types";
import { IIssue, IIssueFilterOptions, TIssuePriorities } from "types";
// fetch-keys
import { USER_ISSUES, WORKSPACE_LABELS } from "constants/fetch-keys";
import { PlusIcon } from "@heroicons/react/24/outline";
@@ -57,8 +57,9 @@ export const MyIssuesView: React.FC<Props> = ({
const { user } = useUserAuth();
const { groupedIssues, mutateMyIssues, isEmpty, params } = useMyIssues(workspaceSlug?.toString());
const { filters, setFilters, issueView, groupBy, orderBy, properties, showEmptyGroups } =
useMyIssuesFilters(workspaceSlug?.toString());
const { filters, setFilters, displayFilters, setDisplayFilters, properties } = useMyIssuesFilters(
workspaceSlug?.toString()
);
const { data: labels } = useSWR(
workspaceSlug && (filters?.labels ?? []).length > 0
@@ -81,7 +82,13 @@ export const MyIssuesView: React.FC<Props> = ({
async (result: DropResult) => {
setTrashBox(false);
if (!result.destination || !workspaceSlug || !groupedIssues || groupBy !== "priority") return;
if (
!result.destination ||
!workspaceSlug ||
!groupedIssues ||
displayFilters?.group_by !== "priority"
)
return;
const { source, destination } = result;
@@ -96,7 +103,7 @@ export const MyIssuesView: React.FC<Props> = ({
const sourceGroup = source.droppableId;
const destinationGroup = destination.droppableId;
draggedItem[groupBy] = destinationGroup;
draggedItem[displayFilters.group_by] = destinationGroup as TIssuePriorities;
mutate<{
[key: string]: IIssue[];
@@ -113,8 +120,14 @@ export const MyIssuesView: React.FC<Props> = ({
return {
...prevData,
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
[sourceGroup]: orderArrayBy(
sourceGroupArray,
displayFilters.order_by ?? "-created_at"
),
[destinationGroup]: orderArrayBy(
destinationGroupArray,
displayFilters.order_by ?? "-created_at"
),
};
},
false
@@ -134,7 +147,7 @@ export const MyIssuesView: React.FC<Props> = ({
.catch(() => mutate(USER_ISSUES(workspaceSlug.toString(), params)));
}
},
[groupBy, groupedIssues, handleDeleteIssue, orderBy, params, user, workspaceSlug]
[displayFilters, groupedIssues, handleDeleteIssue, params, user, workspaceSlug]
);
const addIssueToGroup = useCallback(
@@ -143,19 +156,19 @@ export const MyIssuesView: React.FC<Props> = ({
let preloadedValue: string | string[] = groupTitle;
if (groupBy === "labels") {
if (displayFilters?.group_by === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (groupBy)
if (displayFilters?.group_by)
setPreloadedData({
[groupBy]: preloadedValue,
[displayFilters?.group_by]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData, groupBy]
[setCreateIssueModal, setPreloadedData, displayFilters?.group_by]
);
const addIssueToDate = useCallback(
@@ -263,7 +276,6 @@ export const MyIssuesView: React.FC<Props> = ({
state_group: null,
start_date: null,
target_date: null,
type: null,
})
}
/>
@@ -275,7 +287,7 @@ export const MyIssuesView: React.FC<Props> = ({
addIssueToDate={addIssueToDate}
addIssueToGroup={addIssueToGroup}
disableUserActions={disableUserActions}
dragDisabled={groupBy !== "priority"}
dragDisabled={displayFilters?.group_by !== "priority"}
emptyState={{
title: filters.assignees
? "You don't have any issue assigned to you yet"
@@ -304,15 +316,12 @@ export const MyIssuesView: React.FC<Props> = ({
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
groupByProperty: groupBy,
displayFilters,
groupedIssues,
isEmpty,
issueView,
mutateIssues: mutateMyIssues,
orderBy,
params,
properties,
showEmptyGroups,
}}
/>
</>

View File

@@ -2,7 +2,7 @@
import { observer } from "mobx-react-lite";
// headless ui
import { Disclosure } from "@headlessui/react";
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@@ -19,7 +19,7 @@ import { CustomDatePicker, Icon } from "components/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// types
import { IIssue } from "types";
import { IIssue, TIssuePriorities } from "types";
type Props = {
handleDeleteIssue: () => void;
@@ -66,7 +66,10 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
{mode === "full" && (
<div className="flex justify-between gap-2 pb-3">
<h6 className="flex items-center gap-2 font-medium">
{getStateGroupIcon(issue.state_detail.group, "16", "16", issue.state_detail.color)}
<StateGroupIcon
stateGroup={issue.state_detail.group}
color={issue.state_detail.color}
/>
{issue.project_detail.identifier}-{issue.sequence_id}
</h6>
<div className="flex items-center gap-2">
@@ -114,7 +117,7 @@ export const PeekOverviewIssueProperties: React.FC<Props> = ({
<div className="w-3/4">
<SidebarPrioritySelect
value={issue.priority}
onChange={(val: string) => handleUpdateIssue({ priority: val })}
onChange={(val) => handleUpdateIssue({ priority: val })}
disabled={readOnly}
/>
</div>

View File

@@ -3,12 +3,14 @@ import React from "react";
// ui
import { CustomSelect } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
import { PriorityIcon } from "components/icons/priority-icon";
// types
import { TIssuePriorities } from "types";
// constants
import { PRIORITIES } from "constants/project";
type Props = {
value: string | null;
value: TIssuePriorities;
onChange: (value: string) => void;
};
@@ -18,7 +20,10 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
label={
<div className="flex items-center justify-center gap-2 text-xs">
<span className="flex items-center">
{getPriorityIcon(value, `text-xs ${value ? "" : "text-custom-text-200"}`)}
<PriorityIcon
priority={value}
className={`text-xs ${value ? "" : "text-custom-text-200"}`}
/>
</span>
<span className={`${value ? "" : "text-custom-text-200"} capitalize`}>
{value ?? "Priority"}
@@ -32,7 +37,9 @@ export const IssuePrioritySelect: React.FC<Props> = ({ value, onChange }) => (
<CustomSelect.Option key={priority} value={priority}>
<div className="flex w-full justify-between gap-2 rounded">
<div className="flex items-center justify-start gap-2">
<span>{getPriorityIcon(priority)}</span>
<span>
<PriorityIcon priority={priority} />
</span>
<span className="capitalize">{priority ?? "None"}</span>
</div>
</div>

View File

@@ -10,7 +10,7 @@ import stateService from "services/state.service";
import { CustomSearchSelect } from "components/ui";
// icons
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// fetch keys
@@ -41,7 +41,7 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</div>
),
@@ -58,9 +58,12 @@ export const IssueStateSelect: React.FC<Props> = ({ setIsOpen, value, onChange,
label={
<div className="flex items-center gap-2">
{selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
) : currentDefaultState ? (
getStateGroupIcon(currentDefaultState.group, "16", "16", currentDefaultState.color)
<StateGroupIcon
stateGroup={currentDefaultState.group}
color={currentDefaultState.color}
/>
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}

View File

@@ -3,13 +3,15 @@ import React from "react";
// ui
import { CustomSelect } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
import { PriorityIcon } from "components/icons/priority-icon";
// types
import { TIssuePriorities } from "types";
// constants
import { PRIORITIES } from "constants/project";
type Props = {
value: string | null;
onChange: (val: string) => void;
value: TIssuePriorities;
onChange: (val: TIssuePriorities) => void;
disabled?: boolean;
};
@@ -31,7 +33,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
}`}
>
<span className="grid place-items-center -my-1">
{getPriorityIcon(value ?? "None", "!text-sm")}
<PriorityIcon priority={value} className="!text-sm" />
</span>
<span>{value ?? "None"}</span>
</button>
@@ -44,7 +46,7 @@ export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, disabl
{PRIORITIES.map((option) => (
<CustomSelect.Option key={option} value={option} className="capitalize">
<>
{getPriorityIcon(option, "text-sm")}
<PriorityIcon priority={option} className="text-sm" />
{option ?? "None"}
</>
</CustomSelect.Option>

View File

@@ -9,7 +9,7 @@ import stateService from "services/state.service";
// ui
import { Spinner, CustomSelect } from "components/ui";
// icons
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
import { addSpaceIfCamelCase } from "helpers/string.helper";
@@ -42,17 +42,12 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
<button type="button" className="bg-custom-background-80 text-xs rounded px-2.5 py-0.5">
{selectedState ? (
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
{getStateGroupIcon(
selectedState?.group ?? "backlog",
"14",
"14",
selectedState?.color ?? ""
)}
<StateGroupIcon stateGroup={selectedState.group} color={selectedState.color} />
{addSpaceIfCamelCase(selectedState?.name ?? "")}
</div>
) : inboxIssueId ? (
<div className="flex items-center gap-1.5 text-left text-custom-text-100">
{getStateGroupIcon("backlog", "14", "14", "#ff7700")}
<StateGroupIcon stateGroup="backlog" color="#ff7700" />
Triage
</div>
) : (
@@ -71,7 +66,7 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled
states.map((state) => (
<CustomSelect.Option key={state.id} value={state.id}>
<>
{getStateGroupIcon(state.group, "16", "16", state.color)}
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</>
</CustomSelect.Option>

View File

@@ -401,7 +401,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
render={({ field: { value } }) => (
<SidebarPrioritySelect
value={value}
onChange={(val: string) => submitChanges({ priority: val })}
onChange={(val) => submitChanges({ priority: val })}
disabled={memberRole.isGuest || memberRole.isViewer || uneditable}
/>
)}

View File

@@ -34,7 +34,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug } = router.query;
const { issueView } = useIssuesView();
const { displayFilters } = useIssuesView();
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
@@ -80,7 +80,9 @@ export const ViewDueDateSelect: React.FC<Props> = ({
);
}}
className={`${issue?.target_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
displayFilters.layout === "kanban"
? "bg-custom-background-90"
: "bg-custom-background-100"
}`}
minDate={minDate ?? undefined}
noBorder={noBorder}

View File

@@ -2,18 +2,18 @@ import React from "react";
import { useRouter } from "next/router";
// services
import trackEventServices from "services/track-event.service";
// ui
import { CustomSelect, Tooltip } from "components/ui";
// icons
import { getPriorityIcon } from "components/icons/priority-icon";
import { PriorityIcon } from "components/icons/priority-icon";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { ICurrentUserResponse, IIssue } from "types";
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
// constants
import { PRIORITIES } from "constants/project";
// services
import trackEventServices from "services/track-event.service";
// helper
import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = {
issue: IIssue;
@@ -42,7 +42,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
return (
<CustomSelect
value={issue.priority}
onChange={(data: string) => {
onChange={(data: TIssuePriorities) => {
partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent(
{
@@ -77,9 +77,9 @@ export const ViewPrioritySelect: React.FC<Props> = ({
position={tooltipPosition}
>
<span className="flex gap-1 items-center text-custom-text-200 text-xs">
{getPriorityIcon(
issue.priority && issue.priority !== "" ? issue.priority ?? "" : "None",
`text-sm ${
<PriorityIcon
priority={issue.priority}
className={`text-sm ${
issue.priority === "urgent"
? "text-white"
: issue.priority === "high"
@@ -89,13 +89,9 @@ export const ViewPrioritySelect: React.FC<Props> = ({
: issue.priority === "low"
? "text-green-500"
: "text-custom-text-200"
}`
)}
{noBorder
? issue.priority && issue.priority !== ""
? capitalizeFirstLetter(issue.priority) ?? ""
: "None"
: ""}
}`}
/>
{noBorder ? capitalizeFirstLetter(issue.priority ?? "None") : ""}
</span>
</Tooltip>
</button>
@@ -108,7 +104,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
{PRIORITIES?.map((priority) => (
<CustomSelect.Option key={priority} value={priority} className="capitalize">
<>
{getPriorityIcon(priority, "text-sm")}
<PriorityIcon priority={priority} className="text-sm" />
{priority ?? "None"}
</>
</CustomSelect.Option>

View File

@@ -34,7 +34,7 @@ export const ViewStartDateSelect: React.FC<Props> = ({
const router = useRouter();
const { workspaceSlug } = router.query;
const { issueView } = useIssuesView();
const { displayFilters } = useIssuesView();
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
@@ -72,7 +72,9 @@ export const ViewStartDateSelect: React.FC<Props> = ({
);
}}
className={`${issue?.start_date ? "w-[6.5rem]" : "w-[5rem] text-center"} ${
issueView === "kanban" ? "bg-custom-background-90" : "bg-custom-background-100"
displayFilters.layout === "kanban"
? "bg-custom-background-90"
: "bg-custom-background-100"
}`}
maxDate={maxDate ?? undefined}
noBorder={noBorder}

View File

@@ -10,7 +10,7 @@ import trackEventServices from "services/track-event.service";
// ui
import { CustomSearchSelect, Tooltip } from "components/ui";
// icons
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
@@ -59,7 +59,7 @@ export const ViewStateSelect: React.FC<Props> = ({
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</div>
),
@@ -75,8 +75,9 @@ export const ViewStateSelect: React.FC<Props> = ({
>
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
<span className="h-3.5 w-3.5">
{selectedOption &&
getStateGroupIcon(selectedOption.group, "14", "14", selectedOption.color)}
{selectedOption && (
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
)}
</span>
<span className="truncate">{selectedOption?.name ?? "State"}</span>
</div>

View File

@@ -20,7 +20,7 @@ export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { orderBy } = useIssuesView();
const { displayFilters } = useIssuesView();
const { user } = useUser();
const { projectDetails } = useProjectDetails();
@@ -48,7 +48,7 @@ export const ModuleIssuesGanttChartView: FC<Props> = ({}) => {
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={orderBy === "sort_order" && isAllowed}
enableReorder={displayFilters.order_by === "sort_order" && isAllowed}
bottomSpacing
/>
</div>

View File

@@ -9,13 +9,16 @@ import useToast from "hooks/use-toast";
// services
import userService from "services/user.service";
// ui
import { CustomSelect, Input, PrimaryButton } from "components/ui";
import { CustomSearchSelect, CustomSelect, Input, PrimaryButton } from "components/ui";
// types
import { ICurrentUserResponse, IUser } from "types";
// fetch-keys
import { CURRENT_USER } from "constants/fetch-keys";
// helpers
import { getUserTimeZoneFromWindow } from "helpers/date-time.helper";
// constants
import { USER_ROLES } from "constants/workspace";
import { TIME_ZONES } from "constants/timezones";
const defaultValues: Partial<IUser> = {
first_name: "",
@@ -27,6 +30,12 @@ type Props = {
user?: IUser;
};
const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
value: timeZone.value,
query: timeZone.label + " " + timeZone.value,
content: timeZone.label,
}));
export const UserDetails: React.FC<Props> = ({ user }) => {
const { setToastAlert } = useToast();
@@ -84,6 +93,7 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
user_timezone: getUserTimeZoneFromWindow(),
});
}
}, [user, reset]);
@@ -162,6 +172,34 @@ export const UserDetails: React.FC<Props> = ({ user }) => {
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
</div>
</div>
<div className="space-y-1 text-sm">
<span>What time zone are you in? </span>
<div className="w-full">
<Controller
name="user_timezone"
control={control}
rules={{ required: "This field is required" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value}
label={
value
? TIME_ZONES.find((t) => t.value === value)?.label ?? value
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
verticalPosition="top"
optionsClassName="w-full"
input
/>
)}
/>
{errors?.user_timezone && (
<span className="text-sm text-red-500">{errors.user_timezone.message}</span>
)}
</div>
</div>
</div>
<PrimaryButton type="submit" size="md" disabled={!isValid} loading={isSubmitting}>

View File

@@ -41,16 +41,10 @@ export const ProfileIssuesViewOptions: React.FC = () => {
const { projects } = useProjects();
const {
issueView,
setIssueView,
groupByProperty,
setGroupByProperty,
orderBy,
setOrderBy,
showEmptyGroups,
setShowEmptyGroups,
displayFilters,
setDisplayFilters,
filters,
properties,
displayProperties,
setProperties,
setFilters,
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
@@ -94,11 +88,11 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
issueView === option.type
displayFilters?.layout === option.type
? "bg-custom-sidebar-background-80"
: "text-custom-sidebar-text-200"
}`}
onClick={() => setIssueView(option.type)}
onClick={() => setDisplayFilters({ layout: option.type })}
>
<option.Icon
sx={{
@@ -165,82 +159,89 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<Popover.Panel className="absolute right-0 z-30 mt-1 w-screen max-w-xs transform rounded-lg border border-custom-border-200 bg-custom-background-90 p-3 shadow-lg">
<div className="relative divide-y-2 divide-custom-border-200">
<div className="space-y-4 pb-3 text-xs">
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
groupByProperty === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === groupByProperty
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (issueView === "kanban" && option.key === null) return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Group by</h4>
<div className="w-28">
<CustomMenu
label={
displayFilters?.group_by === "project"
? "Project"
: GROUP_BY_OPTIONS.find(
(option) => option.key === displayFilters?.group_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{GROUP_BY_OPTIONS.map((option) => {
if (displayFilters?.layout === "kanban" && option.key === null)
return null;
if (
option.key === "state" ||
option.key === "created_by" ||
option.key === "assignees"
)
return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => setDisplayFilters({ group_by: option.key })}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
</div>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find((option) => option.key === orderBy)?.name ??
"Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (groupByProperty === "priority" && option.key === "priority")
return null;
if (option.key === "sort_order") return null;
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Order by</h4>
<div className="w-28">
<CustomMenu
label={
ORDER_BY_OPTIONS.find(
(option) => option.key === displayFilters?.order_by
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
>
{ORDER_BY_OPTIONS.map((option) => {
if (
displayFilters?.group_by === "priority" &&
option.key === "priority"
)
return null;
if (option.key === "sort_order") return null;
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setOrderBy(option.key);
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
return (
<CustomMenu.MenuItem
key={option.key}
onClick={() => {
setDisplayFilters({ order_by: option.key });
}}
>
{option.name}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
</div>
</div>
</div>
</>
)}
</>
)}
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Issue type</h4>
<div className="w-28">
<CustomMenu
label={
FILTER_ISSUE_OPTIONS.find((option) => option.key === filters?.type)
?.name ?? "Select"
FILTER_ISSUE_OPTIONS.find(
(option) => option.key === displayFilters?.type
)?.name ?? "Select"
}
className="!w-full"
buttonClassName="w-full"
@@ -249,7 +250,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
<CustomMenu.MenuItem
key={option.key}
onClick={() =>
setFilters({
setDisplayFilters({
type: option.key,
})
}
@@ -261,26 +262,34 @@ export const ProfileIssuesViewOptions: React.FC = () => {
</div>
</div>
{issueView !== "calendar" && issueView !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch value={showEmptyGroups} onChange={setShowEmptyGroups} />
{displayFilters?.layout !== "calendar" &&
displayFilters?.layout !== "spreadsheet" && (
<>
<div className="flex items-center justify-between">
<h4 className="text-custom-text-200">Show empty states</h4>
<div className="w-28">
<ToggleSwitch
value={displayFilters?.show_empty_groups ?? true}
onChange={() =>
setDisplayFilters({
show_empty_groups: !displayFilters?.show_empty_groups,
})
}
/>
</div>
</div>
</div>
</>
)}
</>
)}
</div>
<div className="space-y-2 py-3">
<h4 className="text-sm text-custom-text-200">Display Properties</h4>
<div className="flex flex-wrap items-center gap-2 text-custom-text-200">
{Object.keys(properties).map((key) => {
{Object.keys(displayProperties).map((key) => {
if (key === "estimate" && !isEstimateActive) return null;
if (
issueView === "spreadsheet" &&
displayFilters?.layout === "spreadsheet" &&
(key === "attachment_count" ||
key === "link" ||
key === "sub_issue_count")
@@ -288,7 +297,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
return null;
if (
issueView !== "spreadsheet" &&
displayFilters?.layout !== "spreadsheet" &&
(key === "created_on" || key === "updated_on")
)
return null;
@@ -298,7 +307,7 @@ export const ProfileIssuesViewOptions: React.FC = () => {
key={key}
type="button"
className={`rounded border px-2 py-1 text-xs capitalize ${
properties[key as keyof Properties]
displayProperties[key as keyof Properties]
? "border-custom-primary bg-custom-primary text-white"
: "border-custom-border-200"
}`}

View File

@@ -18,7 +18,7 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue, IIssueFilterOptions } from "types";
import { IIssue, IIssueFilterOptions, TIssuePriorities } from "types";
// fetch-keys
import { USER_PROFILE_PROJECT_SEGREGATION, WORKSPACE_LABELS } from "constants/fetch-keys";
@@ -50,14 +50,12 @@ export const ProfileIssuesView = () => {
const {
groupedIssues,
mutateProfileIssues,
issueView,
groupByProperty,
orderBy,
displayFilters,
setDisplayFilters,
isEmpty,
showEmptyGroups,
filters,
setFilters,
properties,
displayProperties,
params,
} = useProfileIssues(workspaceSlug?.toString(), userId?.toString());
@@ -92,7 +90,12 @@ export const ProfileIssuesView = () => {
async (result: DropResult) => {
setTrashBox(false);
if (!result.destination || !workspaceSlug || !groupedIssues || groupByProperty !== "priority")
if (
!result.destination ||
!workspaceSlug ||
!groupedIssues ||
displayFilters?.group_by !== "priority"
)
return;
const { source, destination } = result;
@@ -108,7 +111,7 @@ export const ProfileIssuesView = () => {
const sourceGroup = source.droppableId;
const destinationGroup = destination.droppableId;
draggedItem[groupByProperty] = destinationGroup;
draggedItem[displayFilters.group_by] = destinationGroup as TIssuePriorities;
mutateProfileIssues((prevData: any) => {
if (!prevData) return prevData;
@@ -121,8 +124,11 @@ export const ProfileIssuesView = () => {
return {
...prevData,
[sourceGroup]: orderArrayBy(sourceGroupArray, orderBy),
[destinationGroup]: orderArrayBy(destinationGroupArray, orderBy),
[sourceGroup]: orderArrayBy(sourceGroupArray, displayFilters.order_by ?? "-created_at"),
[destinationGroup]: orderArrayBy(
destinationGroupArray,
displayFilters.order_by ?? "-created_at"
),
};
}, false);
@@ -140,15 +146,7 @@ export const ProfileIssuesView = () => {
.catch(() => mutateProfileIssues());
}
},
[
groupByProperty,
groupedIssues,
handleDeleteIssue,
mutateProfileIssues,
orderBy,
user,
workspaceSlug,
]
[displayFilters, groupedIssues, handleDeleteIssue, mutateProfileIssues, user, workspaceSlug]
);
const addIssueToGroup = useCallback(
@@ -157,19 +155,19 @@ export const ProfileIssuesView = () => {
let preloadedValue: string | string[] = groupTitle;
if (groupByProperty === "labels") {
if (displayFilters?.group_by === "labels") {
if (groupTitle === "None") preloadedValue = [];
else preloadedValue = [groupTitle];
}
if (groupByProperty)
if (displayFilters?.group_by)
setPreloadedData({
[groupByProperty]: preloadedValue,
[displayFilters?.group_by]: preloadedValue,
actionType: "createIssue",
});
else setPreloadedData({ actionType: "createIssue" });
},
[setCreateIssueModal, setPreloadedData, groupByProperty]
[setCreateIssueModal, setPreloadedData, displayFilters?.group_by]
);
const addIssueToDate = useCallback(
@@ -277,7 +275,6 @@ export const ProfileIssuesView = () => {
state_group: null,
start_date: null,
target_date: null,
type: null,
})
}
/>
@@ -289,7 +286,7 @@ export const ProfileIssuesView = () => {
addIssueToDate={addIssueToDate}
addIssueToGroup={addIssueToGroup}
disableUserActions={false}
dragDisabled={groupByProperty !== "priority"}
dragDisabled={displayFilters?.group_by !== "priority"}
emptyState={{
title: router.pathname.includes("assigned")
? `Issues assigned to ${profileData?.user_data.display_name} will appear here`
@@ -305,15 +302,12 @@ export const ProfileIssuesView = () => {
trashBox={trashBox}
setTrashBox={setTrashBox}
viewProps={{
groupByProperty,
groupedIssues,
displayFilters,
isEmpty,
issueView,
mutateIssues: mutateProfileIssues,
orderBy,
params,
properties,
showEmptyGroups,
properties: displayProperties,
}}
/>
</>

View File

@@ -15,7 +15,7 @@ import {
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
import { groupBy, orderArrayBy } from "helpers/array.helper";
@@ -162,7 +162,7 @@ export const SingleState: React.FC<Props> = ({
return (
<div className="group flex items-center justify-between gap-2 border-custom-border-200 bg-custom-background-100 p-5 first:rounded-t-[10px] last:rounded-b-[10px]">
<div className="flex items-center gap-3">
{getStateGroupIcon(state.group, "20", "20", state.color)}
<StateGroupIcon stateGroup={state.group} color={state.color} height="20px" width="20px" />
<div>
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
<p className="text-xs text-custom-text-200">{state.description}</p>

View File

@@ -56,12 +56,6 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
},
});
useEffect(() => {
if (editor) {
editor.commands.setContent(value);
}
}, [value]);
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({

View File

@@ -13,7 +13,7 @@ import { DateFilterModal } from "components/core";
// ui
import { Avatar, MultiLevelDropdown } from "components/ui";
// icons
import { getPriorityIcon, getStateGroupIcon } from "components/icons";
import { PriorityIcon, StateGroupIcon } from "components/icons";
// helpers
import { getStatesList } from "helpers/state.helper";
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
@@ -99,7 +99,8 @@ export const SelectFilters: React.FC<Props> = ({
id: priority === null ? "null" : priority,
label: (
<div className="flex items-center gap-2 capitalize">
{getPriorityIcon(priority)} {priority ?? "None"}
<PriorityIcon priority={priority} />
{priority ?? "None"}
</div>
),
value: {
@@ -118,7 +119,8 @@ export const SelectFilters: React.FC<Props> = ({
id: state.id,
label: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)} {state.name}
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</div>
),
value: {

View File

@@ -0,0 +1,419 @@
import { useRouter } from "next/router";
// icons
import { Icon, Tooltip } from "components/ui";
import { Squares2X2Icon } from "@heroicons/react/24/outline";
import { BlockedIcon, BlockerIcon } from "components/icons";
// helpers
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IIssueActivity } from "types";
const IssueLink = ({ activity }: { activity: IIssueActivity }) => (
<Tooltip
tooltipContent={
activity.issue_detail ? activity.issue_detail.name : "This issue has been deleted"
}
>
<button
type="button"
onClick={() =>
console.log(
"issue",
JSON.stringify({
project_id: activity.project,
issue_id: activity.issue,
})
)
}
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.issue_detail
? `${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`
: "Issue"}
<Icon iconName="launch" className="!text-xs" />
</button>
</Tooltip>
);
const UserLink = ({ activity }: { activity: IIssueActivity }) => (
<button
type="button"
onClick={() => {
console.log("user", activity.actor);
}}
className="font-medium text-custom-text-100 inline-flex items-center hover:underline"
>
{activity.new_value && activity.new_value !== "" ? activity.new_value : activity.old_value}
</button>
);
const activityDetails: {
[key: string]: {
message: (
activity: IIssueActivity,
showIssue: boolean,
workspaceSlug: string
) => React.ReactNode;
icon: React.ReactNode;
};
} = {
assignees: {
message: (activity, showIssue) => (
<>
{activity.old_value === "" ? "added a new assignee " : "removed the assignee "}
<UserLink activity={activity} />
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="group" className="!text-sm" aria-hidden="true" />,
},
archived_at: {
message: (activity) => {
if (activity.new_value === "restore") return "restored the issue.";
else return "archived the issue.";
},
icon: <Icon iconName="archive" className="!text-sm" aria-hidden="true" />,
},
attachment: {
message: (activity, showIssue) => (
<>
{activity.verb === "created" ? "uploaded a new " : "removed an "}
{activity.new_value && activity.new_value !== "" ? (
<button type="button" onClick={() => console.log("attachment", activity.new_value)}>
attachment
</button>
) : (
"attachment"
)}
{showIssue && activity.verb === "created" ? " to " : " from "}
{showIssue && <IssueLink activity={activity} />}
</>
),
icon: <Icon iconName="attach_file" className="!text-sm" aria-hidden="true" />,
},
blocking: {
message: (activity) => (
<>
{activity.old_value === ""
? "marked this issue is blocking issue "
: "removed the blocking issue "}
<span className="font-medium text-custom-text-100">
{activity.old_value === "" ? activity.new_value : activity.old_value}
</span>
.
</>
),
icon: <BlockerIcon height="12" width="12" color="#6b7280" />,
},
blocks: {
message: (activity) => (
<>
{activity.old_value === ""
? "marked this issue is being blocked by issue "
: "removed this issue being blocked by issue "}
<span className="font-medium text-custom-text-100">
{activity.old_value === "" ? activity.new_value : activity.old_value}
</span>
.
</>
),
icon: <BlockedIcon height="12" width="12" color="#6b7280" />,
},
cycles: {
message: (activity) => (
<>
{activity.verb === "created" && "added this issue to the cycle "}
{activity.verb === "updated" && "set the cycle to "}
{activity.verb === "deleted" && "removed the issue from the cycle "}
<button
type="button"
onClick={() =>
console.log(
"cycle",
JSON.stringify({
cycle_id: activity.new_identifier,
project_id: activity.project,
})
)
}
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
{activity.new_value}
<Icon iconName="launch" className="!text-xs" />
</button>
</>
),
icon: <Icon iconName="contrast" className="!text-sm" aria-hidden="true" />,
},
description: {
message: (activity, showIssue) => (
<>
updated the description
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
estimate_point: {
message: (activity, showIssue) => (
<>
{activity.new_value ? "set the estimate point to " : "removed the estimate point "}
{activity.new_value && (
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
)}
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
</>
),
icon: <Icon iconName="change_history" className="!text-sm" aria-hidden="true" />,
},
issue: {
message: (activity) => {
if (activity.verb === "created") return "created the issue.";
else return "deleted an issue.";
},
icon: <Icon iconName="stack" className="!text-sm" aria-hidden="true" />,
},
labels: {
message: (activity, showIssue) => (
<>
{activity.old_value === "" ? "added a new label " : "removed the label "}
<span className="inline-flex items-center gap-3 rounded-full border border-custom-border-300 px-2 py-0.5 text-xs">
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: "#000000",
}}
aria-hidden="true"
/>
<span className="font-medium text-custom-text-100">
{activity.old_value === "" ? activity.new_value : activity.old_value}
</span>
</span>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
</>
),
icon: <Icon iconName="sell" className="!text-sm" aria-hidden="true" />,
},
link: {
message: (activity, showIssue) => (
<>
{activity.verb === "created" && "added this "}
{activity.verb === "updated" && "updated this "}
{activity.verb === "deleted" && "removed this "}
<button
onClick={() =>
console.log(
"link",
activity.verb === "created" ? activity.new_value : activity.old_value
)
}
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
link
<Icon iconName="launch" className="!text-xs" />
</button>
{showIssue && (
<>
{" "}
to <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="link" className="!text-sm" aria-hidden="true" />,
},
modules: {
message: (activity) => (
<>
{activity.verb === "created" && "added this "}
{activity.verb === "updated" && "updated this "}
{activity.verb === "deleted" && "removed this "}
<button
onClick={() =>
console.log(
"module",
activity.verb === "created" ? activity.new_value : activity.old_value
)
}
className="font-medium text-custom-text-100 inline-flex items-center gap-1 hover:underline"
>
module
<Icon iconName="launch" className="!text-xs" />
</button>
.
</>
),
icon: <Icon iconName="dataset" className="!text-sm" aria-hidden="true" />,
},
name: {
message: (activity, showIssue) => (
<>
set the name to {activity.new_value}
{showIssue && (
<>
{" "}
of <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="chat" className="!text-sm" aria-hidden="true" />,
},
parent: {
message: (activity, showIssue) => (
<>
{activity.new_value ? "set the parent to " : "removed the parent "}
<span className="font-medium text-custom-text-100">
{activity.new_value ? activity.new_value : activity.old_value}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="supervised_user_circle" className="!text-sm" aria-hidden="true" />,
},
priority: {
message: (activity, showIssue) => (
<>
set the priority to{" "}
<span className="font-medium text-custom-text-100">
{activity.new_value ? capitalizeFirstLetter(activity.new_value) : "None"}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Icon iconName="signal_cellular_alt" className="!text-sm" aria-hidden="true" />,
},
start_date: {
message: (activity, showIssue) => (
<>
{activity.new_value ? "set the start date to " : "removed the start date "}
<span className="font-medium text-custom-text-100">
{activity.new_value ? renderShortDateWithYearFormat(activity.new_value) : "None"}
</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
</>
),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
state: {
message: (activity, showIssue) => (
<>
set the state to{" "}
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
.
</>
),
icon: <Squares2X2Icon className="h-3 w-3" aria-hidden="true" />,
},
target_date: {
message: (activity, showIssue) => (
<>
{activity.new_value ? "set the target date to " : "removed the target date "}
{activity.new_value && (
<span className="font-medium text-custom-text-100">
{renderShortDateWithYearFormat(activity.new_value)}
</span>
)}
{showIssue && (
<>
{" "}
for <IssueLink activity={activity} />
</>
)}
</>
),
icon: <Icon iconName="calendar_today" className="!text-sm" aria-hidden="true" />,
},
};
export const ActivityIcon = ({ activity }: { activity: IIssueActivity }) => (
<>{activityDetails[activity.field as keyof typeof activityDetails]?.icon}</>
);
export const ActivityMessage = ({
activity,
showIssue = false,
}: {
activity: IIssueActivity;
showIssue?: boolean;
}) => {
const router = useRouter();
const { workspaceSlug } = router.query;
return (
<>
{activityDetails[activity.field as keyof typeof activityDetails]?.message(
activity,
showIssue,
workspaceSlug?.toString() ?? ""
)}
</>
);
};

View File

@@ -15,3 +15,4 @@ export * from "./add-comment";
export * from "./select-parent";
export * from "./select-blocker";
export * from "./select-blocked";
export * from "./activity-message";

View File

@@ -2,7 +2,6 @@
import React from "react";
// next
import Link from "next/link";
import { useRouter } from "next/router";
// swr
@@ -16,12 +15,10 @@ import issuesService from "services/issues.service";
// hooks
import useUser from "hooks/use-user";
import useToast from "hooks/use-toast";
// components
import { Label, AddComment } from "components/web-view";
import { CommentCard } from "components/issues/comment";
import { ActivityIcon, ActivityMessage } from "components/core";
import { Label, AddComment, ActivityMessage, ActivityIcon } from "components/web-view";
// helpers
import { timeAgo } from "helpers/date-time.helper";
@@ -183,15 +180,15 @@ export const IssueActivity: React.FC<Props> = (props) => {
{activityItem.actor_detail.first_name} Bot
</span>
) : (
<Link
href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}
<button
type="button"
className="text-gray font-medium"
onClick={() => console.log("user", activityItem.actor)}
>
<a className="text-gray font-medium">
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name
: activityItem.actor_detail.display_name}
</a>
</Link>
{activityItem.actor_detail.is_bot
? activityItem.actor_detail.first_name
: activityItem.actor_detail.display_name}
</button>
)}{" "}
{message}{" "}
<span className="whitespace-nowrap">

View File

@@ -109,7 +109,7 @@ export const IssuePropertiesDetail: React.FC<Props> = (props) => {
render={({ field: { value } }) => (
<PrioritySelect
value={value}
onChange={(val: string) => submitChanges({ priority: val })}
onChange={(val) => submitChanges({ priority: val })}
/>
)}
/>

View File

@@ -90,7 +90,7 @@ export const IssueWebViewForm: React.FC<Props> = (props) => {
debouncedTitleSave();
}}
required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
className="min-h-10 block w-full resize-none overflow-hidden rounded border bg-transparent px-3 py-2 text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary"
role="textbox"
disabled={!isAllowed}
/>

View File

@@ -8,15 +8,17 @@ import { ChevronDown } from "lucide-react";
import { PRIORITIES } from "constants/project";
// components
import { getPriorityIcon } from "components/icons";
import { PriorityIcon } from "components/icons";
import { WebViewModal } from "./web-view-modal";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { TIssuePriorities } from "types";
type Props = {
value: any;
onChange: (value: any) => void;
onChange: (value: TIssuePriorities) => void;
disabled?: boolean;
};
@@ -59,7 +61,7 @@ export const PrioritySelect: React.FC<Props> = (props) => {
: "border-custom-border-200 text-custom-text-200"
}`}
>
{getPriorityIcon(priority, "text-sm")}
<PriorityIcon priority={priority} className="text-sm" />
</span>
),
})) || []

View File

@@ -17,7 +17,7 @@ import stateService from "services/state.service";
import { STATES_LIST } from "constants/fetch-keys";
// components
import { getStateGroupIcon } from "components/icons";
import { StateGroupIcon } from "components/icons";
import { WebViewModal } from "./web-view-modal";
// helpers
@@ -62,7 +62,7 @@ export const StateSelect: React.FC<Props> = (props) => {
label: state.name,
value: state.id,
checked: state.id === selectedState?.id,
icon: getStateGroupIcon(state.group, "16", "16", state.color),
icon: <StateGroupIcon stateGroup={state.group} color={state.color} />,
onClick: () => {
setIsOpen(false);
if (disabled) return;

View File

@@ -63,7 +63,7 @@ export const WebViewModal = (props: Props) => {
<XMarkIcon className="w-6 h-6 text-custom-text-200" />
</button>
</div>
<div className="mt-6">{children}</div>
<div className="mt-6 max-h-60 overflow-auto">{children}</div>
</Dialog.Panel>
</Transition.Child>
</div>

View File

@@ -1,9 +1,7 @@
// ui
import { PieGraph } from "components/ui";
// helpers
import { capitalizeFirstLetter } from "helpers/string.helper";
// types
import { IUserStateDistribution } from "types";
import { IUserStateDistribution, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -23,7 +21,7 @@ export const IssuesPieChart: React.FC<Props> = ({ groupedIssues }) => (
id: cell.state_group,
label: cell.state_group,
value: cell.state_count,
color: STATE_GROUP_COLORS[cell.state_group.toLowerCase()],
color: STATE_GROUP_COLORS[cell.state_group.toLowerCase() as TStateGroups],
})) ?? []
}
height="320px"

View File

@@ -1,3 +1,5 @@
import { TIssuePriorities } from "types";
export const NETWORK_CHOICES: { key: 0 | 2; label: string; icon: string }[] = [
{
key: 0,
@@ -19,7 +21,7 @@ export const GROUP_CHOICES = {
cancelled: "Cancelled",
};
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
export const PRIORITIES: TIssuePriorities[] = ["urgent", "high", "medium", "low", null];
export const MONTHS = [
"January",

View File

@@ -1,5 +1,7 @@
import { TStateGroups } from "types";
export const STATE_GROUP_COLORS: {
[key: string]: string;
[key in TStateGroups]: string;
} = {
backlog: "#d9d9d9",
unstarted: "#3f76ff",

View File

@@ -11,14 +11,15 @@ import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
import viewsService from "services/views.service";
// hooks
import useUserAuth from "hooks/use-user-auth";
// types
import {
IIssueFilterOptions,
TIssueViewOptions,
IProjectMember,
TIssueGroupByOptions,
TIssueOrderByOptions,
ICurrentUserResponse,
IIssueDisplayFilterOptions,
IProjectViewProps,
} from "types";
// fetch-keys
import {
@@ -27,66 +28,34 @@ import {
USER_PROJECT_VIEW,
VIEW_DETAILS,
} from "constants/fetch-keys";
import useUserAuth from "hooks/use-user-auth";
export const issueViewContext = createContext<ContextType>({} as ContextType);
type IssueViewProps = {
issueView: TIssueViewOptions;
groupByProperty: TIssueGroupByOptions;
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
calendarDateRange: string;
filters: IIssueFilterOptions;
};
type ReducerActionType = {
type:
| "REHYDRATE_THEME"
| "SET_ISSUE_VIEW"
| "SET_ORDER_BY_PROPERTY"
| "SET_SHOW_EMPTY_STATES"
| "SET_CALENDAR_DATE_RANGE"
| "SET_FILTERS"
| "SET_GROUP_BY_PROPERTY"
| "RESET_TO_DEFAULT"
| "SET_SHOW_SUB_ISSUES";
payload?: Partial<IssueViewProps>;
type: "REHYDRATE_THEME" | "SET_DISPLAY_FILTERS" | "SET_FILTERS" | "RESET_TO_DEFAULT";
payload?: Partial<IProjectViewProps>;
};
type ContextType = IssueViewProps & {
setGroupByProperty: (property: TIssueGroupByOptions) => void;
setOrderBy: (property: TIssueOrderByOptions) => void;
setShowEmptyGroups: (property: boolean) => void;
setShowSubIssues: (value: boolean) => void;
setCalendarDateRange: (property: string) => void;
type ContextType = IProjectViewProps & {
setDisplayFilters: (displayFilter: Partial<IIssueDisplayFilterOptions>) => void;
setFilters: (filters: Partial<IIssueFilterOptions>, saveToServer?: boolean) => void;
resetFilterToDefault: () => void;
setNewFilterDefaultView: () => void;
setIssueView: (property: TIssueViewOptions) => void;
};
type StateType = {
issueView: TIssueViewOptions;
groupByProperty: TIssueGroupByOptions;
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
calendarDateRange: string;
filters: IIssueFilterOptions;
};
type StateType = IProjectViewProps;
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
export const initialState: StateType = {
issueView: "list",
groupByProperty: null,
orderBy: "-created_at",
showEmptyGroups: true,
showSubIssues: true,
calendarDateRange: "",
display_filters: {
calendar_date_range: "",
group_by: null,
layout: "list",
order_by: "-created_at",
show_empty_groups: true,
sub_issue: true,
},
filters: {
type: null,
priority: null,
assignees: null,
labels: null,
@@ -110,70 +79,14 @@ export const reducer: ReducerFunctionType = (state, action) => {
return { ...initialState, ...payload, collapsed };
}
case "SET_ISSUE_VIEW": {
case "SET_DISPLAY_FILTERS": {
const newState = {
...state,
issueView: payload?.issueView || "list",
};
return {
...state,
...newState,
};
}
case "SET_GROUP_BY_PROPERTY": {
const newState = {
...state,
groupByProperty: payload?.groupByProperty || null,
};
return {
...state,
...newState,
};
}
case "SET_ORDER_BY_PROPERTY": {
const newState = {
...state,
orderBy: payload?.orderBy || "-created_at",
};
return {
...state,
...newState,
};
}
case "SET_SHOW_EMPTY_STATES": {
const newState = {
...state,
showEmptyGroups: payload?.showEmptyGroups || true,
};
return {
...state,
...newState,
};
}
case "SET_SHOW_SUB_ISSUES": {
const newState = {
...state,
showSubIssues: payload?.showSubIssues || true,
};
return {
...state,
...newState,
};
}
case "SET_CALENDAR_DATE_RANGE": {
const newState = {
...state,
calendarDateRange: payload?.calendarDateRange || "",
display_filters: {
...state.display_filters,
...payload,
},
issueView: payload?.display_filters?.layout || "list",
};
return {
@@ -210,9 +123,13 @@ export const reducer: ReducerFunctionType = (state, action) => {
}
};
const saveDataToServer = async (workspaceSlug: string, projectID: string, state: any) => {
const saveDataToServer = async (
workspaceSlug: string,
projectId: string,
state: IProjectViewProps
) => {
mutate<IProjectMember>(
workspaceSlug && projectID ? USER_PROJECT_VIEW(projectID as string) : null,
workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId as string) : null,
(prevData) => {
if (!prevData) return prevData;
@@ -224,7 +141,7 @@ const saveDataToServer = async (workspaceSlug: string, projectID: string, state:
false
);
await projectService.setProjectView(workspaceSlug, projectID, {
await projectService.setProjectView(workspaceSlug, projectId, {
view_props: state,
});
};
@@ -354,44 +271,57 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
: null
);
const setIssueView = useCallback(
(property: TIssueViewOptions) => {
const setDisplayFilters = useCallback(
(displayFilter: Partial<IIssueDisplayFilterOptions>) => {
dispatch({
type: "SET_ISSUE_VIEW",
type: "SET_DISPLAY_FILTERS",
payload: {
issueView: property,
display_filters: {
...state.display_filters,
...displayFilter,
},
},
});
const additionalProperties = {
groupByProperty: state.groupByProperty,
orderBy: state.orderBy,
const additionalProperties: Partial<IIssueDisplayFilterOptions> = {
group_by: displayFilter.group_by ?? state.display_filters?.group_by,
order_by: displayFilter.order_by ?? state.display_filters?.order_by,
};
if (property === "kanban" && state.groupByProperty === null) {
additionalProperties.groupByProperty = "state";
if (
displayFilter.layout &&
displayFilter.layout === "kanban" &&
state.display_filters?.group_by === null
) {
additionalProperties.group_by = "state";
dispatch({
type: "SET_GROUP_BY_PROPERTY",
type: "SET_DISPLAY_FILTERS",
payload: {
groupByProperty: "state",
display_filters: {
group_by: "state",
},
},
});
}
if (property === "calendar") {
additionalProperties.groupByProperty = null;
if (displayFilter.layout && displayFilter.layout === "calendar") {
additionalProperties.group_by = null;
dispatch({
type: "SET_GROUP_BY_PROPERTY",
type: "SET_DISPLAY_FILTERS",
payload: {
groupByProperty: null,
display_filters: {
group_by: null,
},
},
});
}
if (property === "gantt_chart") {
additionalProperties.orderBy = "sort_order";
if (displayFilter.layout && displayFilter.layout === "gantt_chart") {
additionalProperties.order_by = "sort_order";
dispatch({
type: "SET_ORDER_BY_PROPERTY",
type: "SET_DISPLAY_FILTERS",
payload: {
orderBy: "sort_order",
display_filters: {
order_by: "sort_order",
},
},
});
}
@@ -400,168 +330,16 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
issueView: property,
...additionalProperties,
display_filters: {
...state.display_filters,
...displayFilter,
...additionalProperties,
},
});
},
[workspaceSlug, projectId, state]
);
const setGroupByProperty = useCallback(
(property: TIssueGroupByOptions) => {
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
groupByProperty: property,
},
});
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
view_props: {
...state,
groupByProperty: property,
},
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
groupByProperty: property,
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setOrderBy = useCallback(
(property: TIssueOrderByOptions) => {
dispatch({
type: "SET_ORDER_BY_PROPERTY",
payload: {
orderBy: property,
},
});
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
view_props: {
...state,
orderBy: property,
},
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
orderBy: property,
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setShowEmptyGroups = useCallback(
(property: boolean) => {
dispatch({
type: "SET_SHOW_EMPTY_STATES",
payload: {
showEmptyGroups: property,
},
});
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
view_props: {
...state,
showEmptyGroups: property,
},
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
showEmptyGroups: property,
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setShowSubIssues = useCallback(
(property: boolean) => {
dispatch({
type: "SET_SHOW_SUB_ISSUES",
payload: {
showSubIssues: property,
},
});
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
view_props: {
...state,
showSubIssues: property,
},
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
showSubIssues: property,
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setCalendarDateRange = useCallback(
(value: string) => {
dispatch({
type: "SET_CALENDAR_DATE_RANGE",
payload: {
calendarDateRange: value,
},
});
if (!workspaceSlug || !projectId) return;
mutateMyViewProps((prevData: any) => {
if (!prevData) return prevData;
return {
...prevData,
view_props: {
...state,
calendarDateRange: value,
},
};
}, false);
saveDataToServer(workspaceSlug as string, projectId as string, {
...state,
calendarDateRange: value,
});
},
[projectId, workspaceSlug, state, mutateMyViewProps]
);
const setFilters = useCallback(
(property: Partial<IIssueFilterOptions>, saveToServer = true) => {
Object.keys(property).forEach((key) => {
@@ -717,9 +495,9 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
payload: myViewProps?.default_props,
});
if (!workspaceSlug || !projectId) return;
if (!workspaceSlug || !projectId || !myViewProps) return;
saveDataToServer(workspaceSlug as string, projectId as string, myViewProps?.default_props);
saveDataToServer(workspaceSlug as string, projectId as string, myViewProps.default_props);
}, [projectId, workspaceSlug, myViewProps]);
useEffect(() => {
@@ -743,22 +521,12 @@ export const IssueViewContextProvider: React.FC<{ children: React.ReactNode }> =
return (
<issueViewContext.Provider
value={{
issueView: state.issueView,
groupByProperty: state.groupByProperty,
setGroupByProperty,
orderBy: state.orderBy,
showEmptyGroups: state.showEmptyGroups,
showSubIssues: state.showSubIssues,
calendarDateRange: state.calendarDateRange,
setCalendarDateRange,
setOrderBy,
setShowEmptyGroups,
setShowSubIssues,
display_filters: state.display_filters,
setDisplayFilters,
filters: state.filters,
setFilters,
resetFilterToDefault: resetToDefault,
setNewFilterDefaultView: setNewDefaultView,
setIssueView,
}}
>
<ToastAlert />

View File

@@ -5,66 +5,36 @@ import ToastAlert from "components/toast-alert";
// types
import {
IIssueFilterOptions,
TIssueViewOptions,
TIssueGroupByOptions,
TIssueOrderByOptions,
Properties,
IWorkspaceViewProps,
IIssueDisplayFilterOptions,
} from "types";
export const profileIssuesContext = createContext<ContextType>({} as ContextType);
type IssueViewProps = {
issueView: TIssueViewOptions;
groupByProperty: TIssueGroupByOptions;
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
filters: IIssueFilterOptions;
properties: Properties;
};
type ReducerActionType = {
type:
| "SET_ISSUE_VIEW"
| "SET_ORDER_BY_PROPERTY"
| "SET_SHOW_EMPTY_STATES"
| "SET_FILTERS"
| "SET_PROPERTIES"
| "SET_GROUP_BY_PROPERTY"
| "RESET_TO_DEFAULT"
| "SET_SHOW_SUB_ISSUES";
payload?: Partial<IssueViewProps>;
type: "SET_DISPLAY_FILTERS" | "SET_FILTERS" | "SET_PROPERTIES" | "RESET_TO_DEFAULT";
payload?: Partial<IWorkspaceViewProps>;
};
type ContextType = IssueViewProps & {
setGroupByProperty: (property: TIssueGroupByOptions) => void;
setOrderBy: (property: TIssueOrderByOptions) => void;
setShowEmptyGroups: (property: boolean) => void;
setShowSubIssues: (value: boolean) => void;
type ContextType = IWorkspaceViewProps & {
setDisplayFilters: (displayFilter: Partial<IIssueDisplayFilterOptions>) => void;
setFilters: (filters: Partial<IIssueFilterOptions>) => void;
setProperties: (key: keyof Properties) => void;
setIssueView: (property: TIssueViewOptions) => void;
};
type StateType = {
issueView: TIssueViewOptions;
groupByProperty: TIssueGroupByOptions;
orderBy: TIssueOrderByOptions;
showEmptyGroups: boolean;
showSubIssues: boolean;
filters: IIssueFilterOptions;
properties: Properties;
};
type StateType = IWorkspaceViewProps;
type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType;
export const initialState: StateType = {
issueView: "list",
groupByProperty: null,
orderBy: "-created_at",
showEmptyGroups: true,
showSubIssues: true,
display_filters: {
group_by: null,
layout: "list",
order_by: "-created_at",
show_empty_groups: true,
sub_issue: true,
},
filters: {
type: null,
priority: null,
assignees: null,
labels: null,
@@ -75,7 +45,7 @@ export const initialState: StateType = {
start_date: null,
target_date: null,
},
properties: {
display_properties: {
assignee: true,
start_date: true,
due_date: true,
@@ -96,58 +66,14 @@ export const reducer: ReducerFunctionType = (state, action) => {
const { type, payload } = action;
switch (type) {
case "SET_ISSUE_VIEW": {
case "SET_DISPLAY_FILTERS": {
const newState = {
...state,
issueView: payload?.issueView || "list",
};
return {
...state,
...newState,
};
}
case "SET_GROUP_BY_PROPERTY": {
const newState = {
...state,
groupByProperty: payload?.groupByProperty || null,
};
return {
...state,
...newState,
};
}
case "SET_ORDER_BY_PROPERTY": {
const newState = {
...state,
orderBy: payload?.orderBy || "-created_at",
};
return {
...state,
...newState,
};
}
case "SET_SHOW_EMPTY_STATES": {
const newState = {
...state,
showEmptyGroups: payload?.showEmptyGroups || true,
};
return {
...state,
...newState,
};
}
case "SET_SHOW_SUB_ISSUES": {
const newState = {
...state,
showSubIssues: payload?.showSubIssues || true,
display_filters: {
...state.display_filters,
...payload,
},
issueView: payload?.display_filters?.layout || "list",
};
return {
@@ -175,8 +101,8 @@ export const reducer: ReducerFunctionType = (state, action) => {
const newState = {
...state,
properties: {
...state.properties,
...payload?.properties,
...state.display_properties,
...payload?.display_properties,
},
};
@@ -197,20 +123,29 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const setIssueView = useCallback(
(property: TIssueViewOptions) => {
const setDisplayFilters = useCallback(
(displayFilter: Partial<IIssueDisplayFilterOptions>) => {
dispatch({
type: "SET_ISSUE_VIEW",
type: "SET_DISPLAY_FILTERS",
payload: {
issueView: property,
display_filters: {
...state.display_filters,
...displayFilter,
},
},
});
if (property === "kanban" && state.groupByProperty === null) {
if (
displayFilter.layout &&
displayFilter.layout === "kanban" &&
state.display_filters?.group_by === null
) {
dispatch({
type: "SET_GROUP_BY_PROPERTY",
type: "SET_DISPLAY_FILTERS",
payload: {
groupByProperty: "state_detail.group",
display_filters: {
group_by: "state_detail.group",
},
},
});
}
@@ -218,42 +153,6 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
[state]
);
const setGroupByProperty = useCallback((property: TIssueGroupByOptions) => {
dispatch({
type: "SET_GROUP_BY_PROPERTY",
payload: {
groupByProperty: property,
},
});
}, []);
const setOrderBy = useCallback((property: TIssueOrderByOptions) => {
dispatch({
type: "SET_ORDER_BY_PROPERTY",
payload: {
orderBy: property,
},
});
}, []);
const setShowEmptyGroups = useCallback((property: boolean) => {
dispatch({
type: "SET_SHOW_EMPTY_STATES",
payload: {
showEmptyGroups: property,
},
});
}, []);
const setShowSubIssues = useCallback((property: boolean) => {
dispatch({
type: "SET_SHOW_SUB_ISSUES",
payload: {
showSubIssues: property,
},
});
}, []);
const setFilters = useCallback(
(property: Partial<IIssueFilterOptions>) => {
Object.keys(property).forEach((key) => {
@@ -279,9 +178,9 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
dispatch({
type: "SET_PROPERTIES",
payload: {
properties: {
...state.properties,
[key]: !state.properties[key],
display_properties: {
...state.display_properties,
[key]: !state.display_properties[key],
},
},
});
@@ -292,19 +191,11 @@ export const ProfileIssuesContextProvider: React.FC<{ children: React.ReactNode
return (
<profileIssuesContext.Provider
value={{
issueView: state.issueView,
setIssueView,
groupByProperty: state.groupByProperty,
setGroupByProperty,
orderBy: state.orderBy,
setOrderBy,
showEmptyGroups: state.showEmptyGroups,
setShowEmptyGroups,
showSubIssues: state.showSubIssues,
setShowSubIssues,
display_filters: state.display_filters,
setDisplayFilters,
filters: state.filters,
setFilters,
properties: state.properties,
display_properties: state.display_properties,
setProperties,
}}
>

View File

@@ -3,7 +3,7 @@ import { BarDatum } from "@nivo/bar";
// helpers
import { capitalizeFirstLetter, generateRandomColor } from "helpers/string.helper";
// types
import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse } from "types";
import { IAnalyticsData, IAnalyticsParams, IAnalyticsResponse, TStateGroups } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
import { MONTHS_LIST } from "constants/calendar";
@@ -72,7 +72,8 @@ export const generateBarColor = (
if (params[type] === "state__name" || params[type] === "labels__name")
color = analytics?.extras?.colors.find((c) => c.name === value)?.color;
if (params[type] === "state__group") color = STATE_GROUP_COLORS[value.toLowerCase()];
if (params[type] === "state__group")
color = STATE_GROUP_COLORS[value.toLowerCase() as TStateGroups];
if (params[type] === "priority") {
const priority = value.toLowerCase();

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