Merge pull request #1406 from makeplane/develop

promote: develop to stage-release
This commit is contained in:
guru_sainath 2023-06-27 16:48:34 +05:30 committed by GitHub
commit fd7274ba1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 681 additions and 356 deletions

View File

@ -15,6 +15,7 @@ from django.db.models import (
Value, Value,
CharField, CharField,
When, When,
Max,
) )
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -195,7 +196,7 @@ class IssueViewSet(BaseViewSet):
output_field=CharField(), output_field=CharField(),
) )
).order_by("priority_order") ).order_by("priority_order")
# State Ordering # State Ordering
elif order_by_param in [ elif order_by_param in [
"state__name", "state__name",
@ -218,6 +219,22 @@ class IssueViewSet(BaseViewSet):
output_field=CharField(), output_field=CharField(),
) )
).order_by("state_order") ).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
@ -239,7 +256,7 @@ class IssueViewSet(BaseViewSet):
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
except Exception as e: except Exception as e:
print(e) capture_exception(e)
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,

View File

@ -1,7 +1,6 @@
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method): def filter_state(params, filter, method):
if method == "GET": if method == "GET":
states = params.get("state").split(",") states = params.get("state").split(",")
@ -26,12 +25,27 @@ def filter_estimate_point(params, filter, method):
def filter_priority(params, filter, method): def filter_priority(params, filter, method):
if method == "GET": if method == "GET":
priorties = params.get("priority").split(",") priorities = params.get("priority").split(",")
if len(priorties) and "" not in priorties: if len(priorities) and "" not in priorities:
filter["priority__in"] = priorties 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"]
else: else:
if params.get("priority", None) and len(params.get("priority")): if params.get("priority", None) and len(params.get("priority")):
filter["priority__in"] = 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"]
return filter return filter

View File

@ -41,6 +41,14 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
colors={(datum) => datum.color} colors={(datum) => datum.color}
curve="monotoneX" curve="monotoneX"
margin={{ top: 20 }} margin={{ top: 20 }}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-brand-secondary"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{ theme={{
background: "rgb(var(--color-bg-base))", background: "rgb(var(--color-bg-base))",
}} }}

View File

@ -23,6 +23,7 @@ import {
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues"; } from "components/issues";
@ -44,7 +45,14 @@ import { LayerDiagonalIcon } from "components/icons";
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// types // types
import { ICurrentUserResponse, IIssue, Properties, TIssueGroupByOptions, UserAuth } from "types"; import {
ICurrentUserResponse,
IIssue,
ISubIssueResponse,
Properties,
TIssueGroupByOptions,
UserAuth,
} from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_DETAILS, CYCLE_DETAILS,
@ -52,6 +60,8 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
type Props = { type Props = {
@ -101,86 +111,68 @@ export const SingleBoardIssue: React.FC<Props> = ({
const { orderBy, params } = useIssuesView(); const { orderBy, params } = useIssuesView();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
if (cycleId) const fetchKey = cycleId
mutate< ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
| { : moduleId
[key: string]: IIssue[]; ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
} : viewId
| IIssue[] ? VIEW_ISSUES(viewId.toString(), params)
>( : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params),
(prevData) => if (issue.parent) {
handleIssuesMutation( mutate<ISubIssueResponse>(
formData, SUB_ISSUES(issue.parent.toString()),
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
),
false
);
else if (moduleId)
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
MODULE_ISSUES_WITH_PARAMS(moduleId as string),
(prevData) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
),
false
);
else {
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
(prevData) => { (prevData) => {
if (!prevData) return prevData; if (!prevData) return prevData;
return handleIssuesMutation( return {
...prevData,
sub_issues: (prevData.sub_issues ?? []).map((i) => {
if (i.id === issue.id) {
return {
...i,
...formData,
};
}
return i;
}),
};
},
false
);
} else {
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
fetchKey,
(prevData) =>
handleIssuesMutation(
formData, formData,
groupTitle ?? "", groupTitle ?? "",
selectedGroup, selectedGroup,
index, index,
orderBy, orderBy,
prevData prevData
); ),
},
false false
); );
} }
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
.then(() => { .then(() => {
if (cycleId) { mutate(fetchKey);
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
} else if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
}); });
}, },
[ [
@ -188,6 +180,7 @@ export const SingleBoardIssue: React.FC<Props> = ({
projectId, projectId,
cycleId, cycleId,
moduleId, moduleId,
viewId,
groupTitle, groupTitle,
index, index,
selectedGroup, selectedGroup,
@ -370,23 +363,15 @@ export const SingleBoardIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && issue.label_details.length > 0 && ( {properties.labels && (
<div className="flex flex-wrap gap-1"> <ViewLabelSelect
{issue.label_details.map((label) => ( issue={issue}
<div partialUpdateIssue={partialUpdateIssue}
key={label.id} isNotAllowed={isNotAllowed}
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary" tooltipPosition="left"
> user={user}
<span selfPositioned
className="h-1.5 w-1.5 flex-shrink-0 rounded-full" />
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</div>
))}
</div>
)} )}
{properties.assignee && ( {properties.assignee && (
<ViewAssigneeSelect <ViewAssigneeSelect

View File

@ -19,6 +19,7 @@ import {
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues"; } from "components/issues";
@ -28,12 +29,13 @@ import { LayerDiagonalIcon } from "components/icons";
// helper // helper
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
// type // type
import { ICurrentUserResponse, IIssue } from "types"; import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_ISSUES_WITH_PARAMS, CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES, VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -68,7 +70,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const fetchKey = cycleId const fetchKey = cycleId
@ -79,25 +81,54 @@ export const SingleCalendarIssue: React.FC<Props> = ({
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
mutate<IIssue[]>( if (issue.parent) {
fetchKey, mutate<ISubIssueResponse>(
(prevData) => SUB_ISSUES(issue.parent.toString()),
(prevData ?? []).map((p) => { (prevData) => {
if (p.id === issueId) { if (!prevData) return prevData;
return {
...p,
...formData,
assignees: formData?.assignees_list ?? p.assignees,
};
}
return p; return {
}), ...prevData,
false sub_issues: (prevData.sub_issues ?? []).map((i) => {
); if (i.id === issue.id) {
return {
...i,
...formData,
};
}
return i;
}),
};
},
false
);
} else {
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) {
return {
...p,
...formData,
assignees: formData?.assignees_list ?? p.assignees,
};
}
return p;
}),
false
);
}
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) .patchIssue(
workspaceSlug as string,
projectId as string,
issue.id as string,
formData,
user
)
.then(() => { .then(() => {
mutate(fetchKey); mutate(fetchKey);
}) })
@ -207,25 +238,14 @@ export const SingleCalendarIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && issue.label_details.length > 0 ? ( {properties.labels && (
<div className="flex flex-wrap gap-1"> <ViewLabelSelect
{issue.label_details.map((label) => ( issue={issue}
<span partialUpdateIssue={partialUpdateIssue}
key={label.id} position="left"
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary" user={user}
> isNotAllowed={isNotAllowed}
<span />
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
) : (
""
)} )}
{properties.assignee && ( {properties.assignee && (
<ViewAssigneeSelect <ViewAssigneeSelect

View File

@ -572,8 +572,11 @@ export const IssuesView: React.FC<Props> = ({
/> />
) : issueView === "spreadsheet" ? ( ) : issueView === "spreadsheet" ? (
<SpreadsheetView <SpreadsheetView
type={type}
handleEditIssue={handleEditIssue} handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue} handleDeleteIssue={handleDeleteIssue}
openIssuesListModal={type !== "issue" ? openIssuesListModal : null}
isCompleted={isCompleted}
user={user} user={user}
userAuth={memberRole} userAuth={memberRole}
/> />

View File

@ -14,6 +14,7 @@ import {
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues/view-select"; } from "components/issues/view-select";
@ -36,7 +37,7 @@ import { LayerDiagonalIcon } from "components/icons";
import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper";
import { handleIssuesMutation } from "constants/issue"; import { handleIssuesMutation } from "constants/issue";
// types // types
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
// fetch-keys // fetch-keys
import { import {
CYCLE_DETAILS, CYCLE_DETAILS,
@ -44,6 +45,8 @@ import {
MODULE_DETAILS, MODULE_DETAILS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
type Props = { type Props = {
@ -80,24 +83,53 @@ export const SingleListIssue: React.FC<Props> = ({
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 });
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { groupByProperty: selectedGroup, orderBy, params } = useIssueView(); const { groupByProperty: selectedGroup, orderBy, params } = useIssueView();
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
if (cycleId) const fetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: viewId
? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
if (issue.parent) {
mutate<ISubIssueResponse>(
SUB_ISSUES(issue.parent.toString()),
(prevData) => {
if (!prevData) return prevData;
return {
...prevData,
sub_issues: (prevData.sub_issues ?? []).map((i) => {
if (i.id === issue.id) {
return {
...i,
...formData,
};
}
return i;
}),
};
},
false
);
} else {
mutate< mutate<
| { | {
[key: string]: IIssue[]; [key: string]: IIssue[];
} }
| IIssue[] | IIssue[]
>( >(
CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params), fetchKey,
(prevData) => (prevData) =>
handleIssuesMutation( handleIssuesMutation(
formData, formData,
@ -109,49 +141,12 @@ export const SingleListIssue: React.FC<Props> = ({
), ),
false false
); );
}
if (moduleId)
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
MODULE_ISSUES_WITH_PARAMS(moduleId as string, params),
(prevData) =>
handleIssuesMutation(
formData,
groupTitle ?? "",
selectedGroup,
index,
orderBy,
prevData
),
false
);
mutate<
| {
[key: string]: IIssue[];
}
| IIssue[]
>(
PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params),
(prevData) =>
handleIssuesMutation(formData, groupTitle ?? "", selectedGroup, index, orderBy, prevData),
false
);
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
.then(() => { .then(() => {
if (cycleId) { mutate(fetchKey);
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params));
mutate(CYCLE_DETAILS(cycleId as string));
} else if (moduleId) {
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
mutate(MODULE_DETAILS(moduleId as string));
} else mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
}); });
}, },
[ [
@ -159,6 +154,7 @@ export const SingleListIssue: React.FC<Props> = ({
projectId, projectId,
cycleId, cycleId,
moduleId, moduleId,
viewId,
groupTitle, groupTitle,
index, index,
selectedGroup, selectedGroup,
@ -275,25 +271,14 @@ export const SingleListIssue: React.FC<Props> = ({
isNotAllowed={isNotAllowed} isNotAllowed={isNotAllowed}
/> />
)} )}
{properties.labels && issue.label_details.length > 0 ? ( {properties.labels && (
<div className="flex flex-wrap gap-1"> <ViewLabelSelect
{issue.label_details.map((label) => ( issue={issue}
<span partialUpdateIssue={partialUpdateIssue}
key={label.id} position="right"
className="group flex items-center gap-1 rounded-2xl border border-brand-base px-2 py-0.5 text-xs text-brand-secondary" user={user}
> isNotAllowed={isNotAllowed}
<span />
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</span>
))}
</div>
) : (
""
)} )}
{properties.assignee && ( {properties.assignee && (
<ViewAssigneeSelect <ViewAssigneeSelect

View File

@ -117,6 +117,14 @@ const ProgressChart: React.FC<Props> = ({ distribution, startDate, endDate, tota
colors={(datum) => datum.color ?? "#3F76FF"} colors={(datum) => datum.color ?? "#3F76FF"}
customYAxisTickValues={[0, totalIssues]} customYAxisTickValues={[0, totalIssues]}
gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))} gridXValues={chartData.map((item, index) => (index % 2 === 0 ? item.currentDate : ""))}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-brand-secondary"> issues pending on </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{ theme={{
background: "transparent", background: "transparent",
axis: { axis: {

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react"; import React, { useCallback, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -10,12 +10,19 @@ import {
ViewAssigneeSelect, ViewAssigneeSelect,
ViewDueDateSelect, ViewDueDateSelect,
ViewEstimateSelect, ViewEstimateSelect,
ViewLabelSelect,
ViewPrioritySelect, ViewPrioritySelect,
ViewStateSelect, ViewStateSelect,
} from "components/issues"; } from "components/issues";
import { Popover2 } from "@blueprintjs/popover2";
// icons // icons
import { CustomMenu, Icon } from "components/ui"; import { Icon } from "components/ui";
import { LinkIcon, PencilIcon, TrashIcon, XMarkIcon } from "@heroicons/react/24/outline"; import {
EllipsisHorizontalIcon,
LinkIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
// hooks // hooks
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
@ -26,10 +33,11 @@ import {
CYCLE_ISSUES_WITH_PARAMS, CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS, PROJECT_ISSUES_LIST_WITH_PARAMS,
SUB_ISSUES,
VIEW_ISSUES, VIEW_ISSUES,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// types // types
import { ICurrentUserResponse, IIssue, Properties, UserAuth } from "types"; import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
// helper // helper
import { copyTextToClipboard } from "helpers/string.helper"; import { copyTextToClipboard } from "helpers/string.helper";
@ -58,6 +66,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
userAuth, userAuth,
nestingLevel, nestingLevel,
}) => { }) => {
const [isOpen, setIsOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
@ -67,7 +76,7 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const fetchKey = cycleId const fetchKey = cycleId
@ -78,25 +87,58 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
? VIEW_ISSUES(viewId.toString(), params) ? VIEW_ISSUES(viewId.toString(), params)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params); : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params);
mutate<IIssue[]>( if (issue.parent) {
fetchKey, mutate<ISubIssueResponse>(
(prevData) => SUB_ISSUES(issue.parent.toString()),
(prevData ?? []).map((p) => { (prevData) => {
if (p.id === issueId) { if (!prevData) return prevData;
return {
...p, return {
...formData, ...prevData,
}; sub_issues: (prevData.sub_issues ?? []).map((i) => {
} if (i.id === issue.id) {
return p; return {
}), ...i,
false ...formData,
); };
}
return i;
}),
};
},
false
);
} else {
mutate<IIssue[]>(
fetchKey,
(prevData) =>
(prevData ?? []).map((p) => {
if (p.id === issue.id) {
return {
...p,
...formData,
};
}
return p;
}),
false
);
}
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, formData, user) .patchIssue(
workspaceSlug as string,
projectId as string,
issue.id as string,
formData,
user
)
.then(() => { .then(() => {
mutate(fetchKey); if (issue.parent) {
mutate(SUB_ISSUES(issue.parent as string));
} else {
mutate(fetchKey);
}
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
@ -128,27 +170,87 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max" className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max"
style={{ gridTemplateColumns }} style={{ gridTemplateColumns }}
> >
<div className="flex gap-1.5 items-center px-4 sticky left-0 z-[1] text-brand-secondary bg-brand-base group-hover:text-brand-base group-hover:bg-brand-surface-2 border-brand-base w-full"> <div className="flex gap-1.5 items-center px-4 sticky z-10 left-0 text-brand-secondary bg-brand-base group-hover:text-brand-base group-hover:bg-brand-surface-2 border-brand-base w-full">
<span className="flex gap-1 items-center" style={issue.parent ? { paddingLeft } : {}}> <div className="flex gap-1.5 items-center" style={issue.parent ? { paddingLeft } : {}}>
<div className="flex items-center cursor-pointer text-xs text-center hover:text-brand-base w-14 opacity-100 group-hover:opacity-0"> <div className="relative flex items-center cursor-pointer text-xs text-center hover:text-brand-base w-14">
{properties.key && ( {properties.key && (
<span> <span className="flex items-center justify-center opacity-100 group-hover:opacity-0">
{issue.project_detail?.identifier}-{issue.sequence_id} {issue.project_detail?.identifier}-{issue.sequence_id}
</span> </span>
)} )}
{!isNotAllowed && (
<div className="absolute top-0 left-2.5 opacity-0 group-hover:opacity-100">
<Popover2
isOpen={isOpen}
canEscapeKeyClose
onInteraction={(nextOpenState) => setIsOpen(nextOpenState)}
content={
<div
className={`flex flex-col gap-1.5 overflow-y-scroll whitespace-nowrap rounded-md border p-1 text-xs shadow-lg focus:outline-none max-h-44 min-w-full border-brand-base bg-brand-surface-1`}
>
<button
type="button"
className="hover:text-brand-muted-1 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2"
onClick={() => {
handleEditIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</button>
<button
type="button"
className="hover:text-brand-muted-1 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2"
onClick={() => {
handleDeleteIssue(issue);
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</button>
<button
type="button"
className="hover:text-brand-muted-1 w-full select-none gap-2 truncate rounded px-1 py-1.5 text-left text-brand-secondary hover:bg-brand-surface-2"
onClick={() => {
handleCopyText();
setIsOpen(false);
}}
>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</button>
</div>
}
placement="bottom-start"
>
<EllipsisHorizontalIcon className="h-5 w-5 text-brand-secondary" />
</Popover2>
</div>
)}
</div> </div>
<div className="h-5 w-5"> <div className="h-6 w-6 flex justify-center items-center">
{issue.sub_issues_count > 0 && ( {issue.sub_issues_count > 0 && (
<button <button
className="h-5 w-5 hover:bg-brand-surface-1 hover:text-brand-base rounded-sm" className="h-5 w-5 hover:bg-brand-surface-1 hover:text-brand-base rounded-sm cursor-pointer"
onClick={() => handleToggleExpand(issue.id)} onClick={() => handleToggleExpand(issue.id)}
> >
<Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} /> <Icon iconName="chevron_right" className={`${expanded ? "rotate-90" : ""}`} />
</button> </button>
)} )}
</div> </div>
</span> </div>
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}> <Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
<a className="truncate text-brand-base cursor-pointer w-full text-[0.825rem]"> <a className="truncate text-brand-base cursor-pointer w-full text-[0.825rem]">
{issue.name} {issue.name}
@ -191,30 +293,19 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
/> />
</div> </div>
)} )}
{properties.labels ? ( {properties.labels && (
issue.label_details.length > 0 ? ( <div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
<div className="flex items-center gap-2 text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base"> <ViewLabelSelect
{issue.label_details.slice(0, 4).map((label, index) => ( issue={issue}
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}> partialUpdateIssue={partialUpdateIssue}
<span position="left"
className={`h-4 w-4 flex-shrink-0 rounded-full border group-hover:bg-brand-surface-2 border-brand-base customButton
`} user={user}
style={{ isNotAllowed={isNotAllowed}
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000", />
}} </div>
/>
</div>
))}
{issue.label_details.length > 4 ? <span>+{issue.label_details.length - 4}</span> : null}
</div>
) : (
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
No Labels
</div>
)
) : (
""
)} )}
{properties.due_date && ( {properties.due_date && (
<div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base"> <div className="flex items-center text-xs text-brand-secondary text-center p-2 group-hover:bg-brand-surface-2 border-brand-base">
<ViewDueDateSelect <ViewDueDateSelect
@ -237,35 +328,6 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
/> />
</div> </div>
)} )}
<div
className="absolute top-2.5 z-10 cursor-pointer opacity-0 group-hover:opacity-100"
style={{
left: `${nestingLevel * 68 + 24}px`,
}}
>
{!isNotAllowed && (
<CustomMenu width="auto" position="left" ellipsis>
<CustomMenu.MenuItem onClick={() => handleEditIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-4 w-4" />
<span>Edit issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleDeleteIssue(issue)}>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-4 w-4" />
<span>Delete issue</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={handleCopyText}>
<div className="flex items-center justify-start gap-2">
<LinkIcon className="h-4 w-4" />
<span>Copy issue link</span>
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</div> </div>
); );
}; };

View File

@ -40,7 +40,7 @@ export const SpreadsheetColumns: React.FC<Props> = ({ columnData, gridTemplateCo
return ( return (
<div <div
className={`bg-brand-surface-1 w-full ${ className={`bg-brand-surface-1 w-full ${
col.propertyName === "title" ? "sticky left-0 z-[2] bg-brand-surface-1 pl-24" : "" col.propertyName === "title" ? "sticky left-0 z-20 bg-brand-surface-1 pl-24" : ""
}`} }`}
> >
{col.propertyName === "title" ? ( {col.propertyName === "title" ? (

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
// components // components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { Icon, Spinner } from "components/ui"; import { CustomMenu, Icon, Spinner } from "components/ui";
// hooks // hooks
import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesProperties from "hooks/use-issue-properties";
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view"; import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
@ -17,15 +17,21 @@ import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
import { PlusIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
type Props = { type Props = {
type: "issue" | "cycle" | "module";
handleEditIssue: (issue: IIssue) => void; handleEditIssue: (issue: IIssue) => void;
handleDeleteIssue: (issue: IIssue) => void; handleDeleteIssue: (issue: IIssue) => void;
openIssuesListModal?: (() => void) | null;
isCompleted?: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
userAuth: UserAuth; userAuth: UserAuth;
}; };
export const SpreadsheetView: React.FC<Props> = ({ export const SpreadsheetView: React.FC<Props> = ({
type,
handleEditIssue, handleEditIssue,
handleDeleteIssue, handleDeleteIssue,
openIssuesListModal,
isCompleted = false,
user, user,
userAuth, userAuth,
}) => { }) => {
@ -56,7 +62,7 @@ export const SpreadsheetView: React.FC<Props> = ({
return ( return (
<div className="h-full rounded-lg text-brand-secondary overflow-x-auto whitespace-nowrap bg-brand-base"> <div className="h-full rounded-lg text-brand-secondary overflow-x-auto whitespace-nowrap bg-brand-base">
<div className="sticky z-[2] top-0 border-b border-brand-base bg-brand-surface-1 w-full min-w-max"> <div className="sticky z-20 top-0 border-b border-brand-base bg-brand-surface-1 w-full min-w-max">
<SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} /> <SpreadsheetColumns columnData={columnData} gridTemplateColumns={gridTemplateColumns} />
</div> </div>
{spreadsheetIssues ? ( {spreadsheetIssues ? (
@ -75,16 +81,55 @@ export const SpreadsheetView: React.FC<Props> = ({
userAuth={userAuth} userAuth={userAuth}
/> />
))} ))}
<button <div
className="flex items-center gap-1.5 pl-7 py-2.5 text-sm text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max" className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-brand-surface-2 border-b border-brand-base w-full min-w-max"
onClick={() => { style={{ gridTemplateColumns }}
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
> >
<PlusIcon className="h-4 w-4" /> {type === "issue" ? (
Add Issue <button
</button> className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-brand-secondary bg-brand-base group-hover:text-brand-base group-hover:bg-brand-surface-2 border-brand-base w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
!isCompleted && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-brand-secondary bg-brand-base group-hover:text-brand-base group-hover:bg-brand-surface-2 border-brand-base w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
menuItemsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
<CustomMenu.MenuItem onClick={openIssuesListModal}>
Add an existing issue
</CustomMenu.MenuItem>
)}
</CustomMenu>
)
)}
</div>
</div> </div>
) : ( ) : (
<Spinner /> <Spinner />

View File

@ -23,7 +23,6 @@ import {
IssueStateSelect, IssueStateSelect,
} from "components/issues/select"; } from "components/issues/select";
import { CreateStateModal } from "components/states"; import { CreateStateModal } from "components/states";
import { CreateUpdateCycleModal } from "components/cycles";
import { CreateLabelModal } from "components/labels"; import { CreateLabelModal } from "components/labels";
// ui // ui
import { import {
@ -73,7 +72,6 @@ const defaultValues: Partial<IIssue> = {
description_html: "<p></p>", description_html: "<p></p>",
estimate_point: null, estimate_point: null,
state: "", state: "",
cycle: null,
priority: null, priority: null,
assignees: [], assignees: [],
assignees_list: [], assignees_list: [],
@ -122,7 +120,6 @@ export const IssueForm: FC<IssueFormProps> = ({
}) => { }) => {
// states // states
const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>(); const [mostSimilarIssue, setMostSimilarIssue] = useState<IIssue | undefined>();
const [cycleModal, setCycleModal] = useState(false);
const [stateModal, setStateModal] = useState(false); const [stateModal, setStateModal] = useState(false);
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@ -252,11 +249,6 @@ export const IssueForm: FC<IssueFormProps> = ({
projectId={projectId} projectId={projectId}
user={user} user={user}
/> />
<CreateUpdateCycleModal
isOpen={cycleModal}
handleClose={() => setCycleModal(false)}
user={user}
/>
<CreateLabelModal <CreateLabelModal
isOpen={labelModal} isOpen={labelModal}
handleClose={() => setLabelModal(false)} handleClose={() => setLabelModal(false)}

View File

@ -82,12 +82,17 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { params: inboxParams } = useInboxView(); const { params: inboxParams } = useInboxView();
const { params: spreadsheetParams } = useSpreadsheetIssuesView(); const { params: spreadsheetParams } = useSpreadsheetIssuesView();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
const { user } = useUser(); const { user } = useUser();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string };
if (router.asPath.includes("my-issues"))
prePopulateData = {
...prePopulateData,
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
};
const { data: issues } = useSWR( const { data: issues } = useSWR(
workspaceSlug && activeProject workspaceSlug && activeProject
? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "") ? PROJECT_ISSUES_LIST(workspaceSlug as string, activeProject ?? "")
@ -121,7 +126,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}, [handleClose]); }, [handleClose]);
const addIssueToCycle = async (issueId: string, cycleId: string) => { const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !activeProject) return;
await issuesService await issuesService
.addIssueToCycle( .addIssueToCycle(
@ -142,7 +147,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}; };
const addIssueToModule = async (issueId: string, moduleId: string) => { const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !activeProject) return;
await modulesService await modulesService
.addIssuesToModule( .addIssuesToModule(
@ -163,7 +168,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}; };
const addIssueToInbox = async (formData: Partial<IIssue>) => { const addIssueToInbox = async (formData: Partial<IIssue>) => {
if (!workspaceSlug || !projectId || !inboxId) return; if (!workspaceSlug || !activeProject || !inboxId) return;
const payload = { const payload = {
issue: { issue: {
@ -178,7 +183,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
await inboxServices await inboxServices
.createInboxIssue( .createInboxIssue(
workspaceSlug.toString(), workspaceSlug.toString(),
projectId.toString(), activeProject.toString(),
inboxId.toString(), inboxId.toString(),
payload, payload,
user user
@ -191,7 +196,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}); });
router.push( router.push(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}` `/${workspaceSlug}/projects/${activeProject}/inbox/${inboxId}?inboxIssueId=${res.issue_inbox[0].id}`
); );
mutate(INBOX_ISSUES(inboxId.toString(), inboxParams)); mutate(INBOX_ISSUES(inboxId.toString(), inboxParams));
@ -211,7 +216,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), calendarParams) ? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams); : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const spreadsheetFetchKey = cycleId const spreadsheetFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams)
@ -219,7 +224,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams)
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), spreadsheetParams) ? VIEW_ISSUES(viewId.toString(), spreadsheetParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", spreadsheetParams); : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", spreadsheetParams);
const ganttFetchKey = cycleId const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
@ -227,10 +232,10 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId : viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams) ? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? ""); : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
const createIssue = async (payload: Partial<IIssue>) => { const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !activeProject) return;
if (inboxId) await addIssueToInbox(payload); if (inboxId) await addIssueToInbox(payload);
else else
@ -252,7 +257,8 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
if (payload.assignees_list?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE); if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
}) })

View File

@ -44,14 +44,14 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const partialUpdateIssue = useCallback( const partialUpdateIssue = useCallback(
(formData: Partial<IIssue>, issueId: string) => { (formData: Partial<IIssue>, issue: IIssue) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
mutate<IIssue[]>( mutate<IIssue[]>(
USER_ISSUE(workspaceSlug as string), USER_ISSUE(workspaceSlug as string),
(prevData) => (prevData) =>
prevData?.map((p) => { prevData?.map((p) => {
if (p.id === issueId) return { ...p, ...formData }; if (p.id === issue.id) return { ...p, ...formData };
return p; return p;
}), }),
@ -59,7 +59,7 @@ export const MyIssuesListItem: React.FC<Props> = ({ issue, properties, projectId
); );
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId, formData, user) .patchIssue(workspaceSlug as string, projectId as string, issue.id, formData, user)
.then((res) => { .then((res) => {
mutate(USER_ISSUE(workspaceSlug as string)); mutate(USER_ISSUE(workspaceSlug as string));
}) })

View File

@ -18,7 +18,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void; partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right"; position?: "left" | "right";
selfPositioned?: boolean; selfPositioned?: boolean;
tooltipPosition?: "left" | "right"; tooltipPosition?: "left" | "right";
@ -108,7 +108,7 @@ export const ViewAssigneeSelect: React.FC<Props> = ({
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1); if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
else newData.push(data); else newData.push(data);
partialUpdateIssue({ assignees_list: data }, issue.id); partialUpdateIssue({ assignees_list: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {

View File

@ -11,7 +11,7 @@ import { ICurrentUserResponse, IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void; partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
noBorder?: boolean; noBorder?: boolean;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
isNotAllowed: boolean; isNotAllowed: boolean;
@ -48,7 +48,7 @@ export const ViewDueDateSelect: React.FC<Props> = ({
priority: issue.priority, priority: issue.priority,
state: issue.state, state: issue.state,
}, },
issue.id issue
); );
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {

View File

@ -15,7 +15,7 @@ import { ICurrentUserResponse, IIssue } from "types";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void; partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right"; position?: "left" | "right";
selfPositioned?: boolean; selfPositioned?: boolean;
customButton?: boolean; customButton?: boolean;
@ -54,7 +54,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
<CustomSelect <CustomSelect
value={issue.estimate_point} value={issue.estimate_point}
onChange={(val: number) => { onChange={(val: number) => {
partialUpdateIssue({ estimate_point: val }, issue.id); partialUpdateIssue({ estimate_point: val }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug, workspaceSlug,

View File

@ -3,3 +3,4 @@ export * from "./due-date";
export * from "./estimate"; export * from "./estimate";
export * from "./priority"; export * from "./priority";
export * from "./state"; export * from "./state";
export * from "./label";

View File

@ -0,0 +1,148 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// services
import issuesService from "services/issues.service";
// component
import { CreateLabelModal } from "components/labels";
// ui
import { CustomSearchSelect, Tooltip } from "components/ui";
// icons
import { PlusIcon, TagIcon } from "@heroicons/react/24/outline";
// types
import { ICurrentUserResponse, IIssue, IIssueLabels } from "types";
// fetch-keys
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = {
issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right";
selfPositioned?: boolean;
tooltipPosition?: "left" | "right";
customButton?: boolean;
user: ICurrentUserResponse | undefined;
isNotAllowed: boolean;
};
export const ViewLabelSelect: React.FC<Props> = ({
issue,
partialUpdateIssue,
position = "left",
selfPositioned = false,
tooltipPosition = "right",
user,
isNotAllowed,
customButton = false,
}) => {
const [labelModal, setLabelModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: issueLabels } = useSWR<IIssueLabels[]>(
projectId ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
workspaceSlug && projectId
? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string)
: null
);
const options = issueLabels?.map((label) => ({
value: label.id,
query: label.name,
content: (
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span>{label.name}</span>
</div>
),
}));
const labelsLabel = (
<Tooltip
position={`top-${tooltipPosition}`}
tooltipHeading="Labels"
tooltipContent={
issue.label_details.length > 0
? issue.label_details.map((label) => label.name ?? "").join(", ")
: "No Label"
}
>
<div
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer"
} items-center gap-2 text-brand-secondary`}
>
{issue.label_details.length > 0 ? (
<>
{issue.label_details.slice(0, 4).map((label, index) => (
<div className={`flex h-4 w-4 rounded-full ${index ? "-ml-3.5" : ""}`}>
<span
className={`h-4 w-4 flex-shrink-0 rounded-full border group-hover:bg-brand-surface-2 border-brand-base
`}
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000000",
}}
/>
</div>
))}
{issue.label_details.length > 4 ? <span>+{issue.label_details.length - 4}</span> : null}
</>
) : (
<>
<TagIcon className="h-3.5 w-3.5 text-brand-secondary" />
</>
)}
</div>
</Tooltip>
);
const footerOption = (
<button
type="button"
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-brand-surface-2"
onClick={() => setLabelModal(true)}
>
<span className="flex items-center justify-start gap-1 text-brand-secondary">
<PlusIcon className="h-4 w-4" aria-hidden="true" />
<span>Create New Label</span>
</span>
</button>
);
return (
<>
{projectId && (
<CreateLabelModal
isOpen={labelModal}
handleClose={() => setLabelModal(false)}
projectId={projectId.toString()}
user={user}
/>
)}
<CustomSearchSelect
value={issue.labels}
onChange={(data: string[]) => {
partialUpdateIssue({ labels_list: data }, issue);
}}
options={options}
{...(customButton ? { customButton: labelsLabel } : { label: labelsLabel })}
multiple
noChevron
position={position}
disabled={isNotAllowed}
selfPositioned={selfPositioned}
footerOption={footerOption}
dropdownWidth="w-full min-w-[12rem]"
/>
</>
);
};

View File

@ -17,7 +17,7 @@ import { capitalizeFirstLetter } from "helpers/string.helper";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void; partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right"; position?: "left" | "right";
selfPositioned?: boolean; selfPositioned?: boolean;
noBorder?: boolean; noBorder?: boolean;
@ -41,7 +41,7 @@ export const ViewPrioritySelect: React.FC<Props> = ({
<CustomSelect <CustomSelect
value={issue.priority} value={issue.priority}
onChange={(data: string) => { onChange={(data: string) => {
partialUpdateIssue({ priority: data }, issue.id); partialUpdateIssue({ priority: data }, issue);
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {
workspaceSlug, workspaceSlug,

View File

@ -19,7 +19,7 @@ import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
issue: IIssue; issue: IIssue;
partialUpdateIssue: (formData: Partial<IIssue>, issueId: string) => void; partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
position?: "left" | "right"; position?: "left" | "right";
selfPositioned?: boolean; selfPositioned?: boolean;
customButton?: boolean; customButton?: boolean;
@ -83,7 +83,7 @@ export const ViewStateSelect: React.FC<Props> = ({
priority: issue.priority, priority: issue.priority,
target_date: issue.target_date, target_date: issue.target_date,
}, },
issue.id issue
); );
trackEventServices.trackIssuePartialPropertyUpdateEvent( trackEventServices.trackIssuePartialPropertyUpdateEvent(
{ {

View File

@ -15,7 +15,7 @@ import stateService from "services/state.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui // ui
import { CustomSelect, Input, PrimaryButton, SecondaryButton } from "components/ui"; import { CustomSelect, Input, PrimaryButton, SecondaryButton, Tooltip } from "components/ui";
// types // types
import type { ICurrentUserResponse, IState, IStateResponse } from "types"; import type { ICurrentUserResponse, IState, IStateResponse } from "types";
// fetch-keys // fetch-keys
@ -28,6 +28,7 @@ type Props = {
onClose: () => void; onClose: () => void;
selectedGroup: StateGroup | null; selectedGroup: StateGroup | null;
user: ICurrentUserResponse | undefined; user: ICurrentUserResponse | undefined;
groupLength: number;
}; };
export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null; export type StateGroup = "backlog" | "unstarted" | "started" | "completed" | "cancelled" | null;
@ -43,6 +44,7 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
onClose, onClose,
selectedGroup, selectedGroup,
user, user,
groupLength,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
@ -174,9 +176,8 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
{({ open }) => ( {({ open }) => (
<> <>
<Popover.Button <Popover.Button
className={`group inline-flex items-center text-base font-medium focus:outline-none ${ className={`group inline-flex items-center text-base font-medium focus:outline-none ${open ? "text-brand-base" : "text-brand-secondary"
open ? "text-brand-base" : "text-brand-secondary" }`}
}`}
> >
{watch("color") && watch("color") !== "" && ( {watch("color") && watch("color") !== "" && (
<span <span
@ -228,22 +229,27 @@ export const CreateUpdateStateInline: React.FC<Props> = ({
name="group" name="group"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<CustomSelect <Tooltip tooltipContent={groupLength === 1 ? "Cannot have an empty group." : "Choose State"} >
value={value} <div>
onChange={onChange} <CustomSelect
label={ disabled={groupLength === 1}
Object.keys(GROUP_CHOICES).find((k) => k === value.toString()) value={value}
? GROUP_CHOICES[value.toString() as keyof typeof GROUP_CHOICES] onChange={onChange}
: "Select group" label={
} Object.keys(GROUP_CHOICES).find((k) => k === value.toString())
input ? GROUP_CHOICES[value.toString() as keyof typeof GROUP_CHOICES]
> : "Select group"
{Object.keys(GROUP_CHOICES).map((key) => ( }
<CustomSelect.Option key={key} value={key}> input
{GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]} >
</CustomSelect.Option> {Object.keys(GROUP_CHOICES).map((key) => (
))} <CustomSelect.Option key={key} value={key}>
</CustomSelect> {GROUP_CHOICES[key as keyof typeof GROUP_CHOICES]}
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</Tooltip>
)} )}
/> />
)} )}

View File

@ -19,6 +19,7 @@ type Props = {
noChevron?: boolean; noChevron?: boolean;
position?: "left" | "right"; position?: "left" | "right";
verticalPosition?: "top" | "bottom"; verticalPosition?: "top" | "bottom";
menuItemsClassName?: string;
customButton?: JSX.Element; customButton?: JSX.Element;
menuItemsWhiteBg?: boolean; menuItemsWhiteBg?: boolean;
}; };
@ -44,6 +45,7 @@ const CustomMenu = ({
noChevron = false, noChevron = false,
position = "right", position = "right",
verticalPosition = "bottom", verticalPosition = "bottom",
menuItemsClassName = "",
customButton, customButton,
menuItemsWhiteBg = false, menuItemsWhiteBg = false,
}: Props) => ( }: Props) => (
@ -133,7 +135,7 @@ const CustomMenu = ({
menuItemsWhiteBg menuItemsWhiteBg
? "border-brand-surface-1 bg-brand-base" ? "border-brand-surface-1 bg-brand-base"
: "border-brand-base bg-brand-surface-1" : "border-brand-base bg-brand-surface-1"
}`} } ${menuItemsClassName}`}
> >
<div className="py-1">{children}</div> <div className="py-1">{children}</div>
</Menu.Items> </Menu.Items>

View File

@ -54,7 +54,7 @@ const CustomSelect = ({
) : ( ) : (
<Listbox.Button <Listbox.Button
className={`flex w-full ${ className={`flex w-full ${
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-brand-surface-2" disabled ? "cursor-not-allowed text-brand-secondary" : "cursor-pointer hover:bg-brand-surface-2"
} items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 focus:outline-none ${ } items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 focus:outline-none ${
input ? "border-brand-base px-3 py-2 text-sm" : "px-2.5 py-1 text-xs" input ? "border-brand-base px-3 py-2 text-sm" : "px-2.5 py-1 text-xs"
} ${ } ${

View File

@ -3,7 +3,7 @@ import { Fragment, useState } from "react";
// headless ui // headless ui
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
// icons // icons
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
type MultiLevelDropdownProps = { type MultiLevelDropdownProps = {
@ -127,9 +127,14 @@ export const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
}} }}
className={`${ className={`${
child.selected ? "bg-brand-surface-2" : "" child.selected ? "bg-brand-surface-2" : ""
} flex w-full items-center whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`} } flex w-full items-center justify-between whitespace-nowrap break-words rounded px-1 py-1.5 text-left capitalize text-brand-secondary hover:bg-brand-surface-2`}
> >
{child.label} {child.label}
<CheckIcon
className={`h-3.5 w-3.5 opacity-0 ${
child.selected ? "opacity-100" : ""
}`}
/>
</button> </button>
))} ))}
</div> </div>

View File

@ -70,7 +70,7 @@ export const SelectFilters: React.FC<Props> = ({
value: PRIORITIES, value: PRIORITIES,
children: [ children: [
...PRIORITIES.map((priority) => ({ ...PRIORITIES.map((priority) => ({
id: priority ?? "none", id: priority === null ? "null" : priority,
label: ( label: (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getPriorityIcon(priority)} {priority ?? "None"} {getPriorityIcon(priority)} {priority ?? "None"}
@ -78,9 +78,9 @@ export const SelectFilters: React.FC<Props> = ({
), ),
value: { value: {
key: "priority", key: "priority",
value: priority, value: priority === null ? "null" : priority,
}, },
selected: filters?.priority?.includes(priority ?? "none"), selected: filters?.priority?.includes(priority === null ? "null" : priority),
})), })),
], ],
}, },

View File

@ -60,6 +60,14 @@ export const CompletedIssuesGraph: React.FC<Props> = ({ month, issues, setMonth
margin={{ top: 20, right: 20, bottom: 20, left: 20 }} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
customYAxisTickValues={data.map((item) => item.completed_count)} customYAxisTickValues={data.map((item) => item.completed_count)}
colors={(datum) => datum.color} colors={(datum) => datum.color}
enableSlices="x"
sliceTooltip={(datum) => (
<div className="rounded-md border border-brand-base bg-brand-surface-2 p-2 text-xs">
{datum.slice.points[0].data.yFormatted}
<span className="text-brand-secondary"> issues closed in </span>
{datum.slice.points[0].data.xFormatted}
</div>
)}
theme={{ theme={{
background: "rgb(var(--color-bg-base))", background: "rgb(var(--color-bg-base))",
}} }}

View File

@ -6,10 +6,15 @@ type Props = {
left?: JSX.Element; left?: JSX.Element;
right?: JSX.Element; right?: JSX.Element;
setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>; setToggleSidebar: React.Dispatch<React.SetStateAction<boolean>>;
noHeader: boolean;
}; };
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => ( const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar, noHeader }) => (
<div className="relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border-b border-brand-base bg-brand-sidebar px-5 py-4"> <div
className={`relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border-b border-brand-base bg-brand-sidebar px-5 py-4 ${
noHeader ? "md:hidden" : ""
}`}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="block md:hidden"> <div className="block md:hidden">
<button <button

View File

@ -100,14 +100,13 @@ const ProjectAuthorizationWrapped: React.FC<Props> = ({
: "bg-brand-base" : "bg-brand-base"
}`} }`}
> >
{!noHeader && ( <AppHeader
<AppHeader breadcrumbs={breadcrumbs}
breadcrumbs={breadcrumbs} left={left}
left={left} right={right}
right={right} setToggleSidebar={setToggleSidebar}
setToggleSidebar={setToggleSidebar} noHeader={noHeader}
/> />
)}
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<div className="h-full w-full overflow-x-hidden overflow-y-scroll">{children}</div> <div className="h-full w-full overflow-x-hidden overflow-y-scroll">{children}</div>
</div> </div>

View File

@ -107,14 +107,13 @@ export const WorkspaceAuthorizationLayout: React.FC<Props> = ({
: "bg-brand-base" : "bg-brand-base"
}`} }`}
> >
{!noHeader && ( <AppHeader
<AppHeader breadcrumbs={breadcrumbs}
breadcrumbs={breadcrumbs} left={left}
left={left} right={right}
right={right} setToggleSidebar={setToggleSidebar}
setToggleSidebar={setToggleSidebar} noHeader={noHeader}
/> />
)}
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<div className="relative h-full w-full overflow-x-hidden overflow-y-scroll"> <div className="relative h-full w-full overflow-x-hidden overflow-y-scroll">
{children} {children}

View File

@ -98,6 +98,7 @@ const StatesSettings: NextPage = () => {
<div className="divide-y divide-brand-base rounded-[10px] border border-brand-base"> <div className="divide-y divide-brand-base rounded-[10px] border border-brand-base">
{key === activeGroup && ( {key === activeGroup && (
<CreateUpdateStateInline <CreateUpdateStateInline
groupLength={orderedStateGroups[key].length}
onClose={() => { onClose={() => {
setActiveGroup(null); setActiveGroup(null);
setSelectedState(null); setSelectedState(null);
@ -128,6 +129,7 @@ const StatesSettings: NextPage = () => {
setActiveGroup(null); setActiveGroup(null);
setSelectedState(null); setSelectedState(null);
}} }}
groupLength={orderedStateGroups[key].length}
data={ data={
statesList?.find((state) => state.id === selectedState) ?? null statesList?.find((state) => state.id === selectedState) ?? null
} }

View File

@ -225,3 +225,8 @@ body {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
} }
/* popover2 styling */
.bp4-popover2-transition-container {
z-index: 20 !important;
}