forked from github/plane
Merge branch 'develop' into packaging-tiptap
This commit is contained in:
commit
5f8a0c3f94
@ -130,7 +130,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch = int(timezone.now().time())
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
@ -330,7 +330,12 @@ class IssueViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
try:
|
||||
issue = Issue.issue_objects.get(
|
||||
issue = Issue.issue_objects.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
).get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
@ -2412,7 +2417,7 @@ class IssueDraftViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch = int(timezone.now().time())
|
||||
epoch = int(timezone.now().timestamp())
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
@ -1,9 +1,6 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-15 06:55
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_issue_activity(apps, schema_editor):
|
||||
@ -23,27 +20,5 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalView',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||
('query', models.JSONField(verbose_name='View Query')),
|
||||
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||
('query_data', models.JSONField(default=dict)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global View',
|
||||
'verbose_name_plural': 'Global Views',
|
||||
'db_table': 'global_views',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(update_issue_activity),
|
||||
]
|
||||
|
@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-14 06:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def update_epoch(apps, schema_editor):
|
||||
IssueActivity = apps.get_model('db', 'IssueActivity')
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.all():
|
||||
obj.epoch = int(obj.created_at.timestamp())
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0044_auto_20230913_0709'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='epoch',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.RunPython(update_epoch),
|
||||
]
|
53
apiserver/plane/db/migrations/0046_auto_20230919_1421.py
Normal file
53
apiserver/plane/db/migrations/0046_auto_20230919_1421.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Generated by Django 4.2.3 on 2023-09-19 14:21
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
def update_epoch(apps, schema_editor):
|
||||
IssueActivity = apps.get_model('db', 'IssueActivity')
|
||||
updated_issue_activity = []
|
||||
for obj in IssueActivity.objects.all():
|
||||
obj.epoch = int(obj.created_at.timestamp())
|
||||
updated_issue_activity.append(obj)
|
||||
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0045_auto_20230915_0655'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GlobalView',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='View Name')),
|
||||
('description', models.TextField(blank=True, verbose_name='View Description')),
|
||||
('query', models.JSONField(verbose_name='View Query')),
|
||||
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
|
||||
('query_data', models.JSONField(default=dict)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='global_views', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global View',
|
||||
'verbose_name_plural': 'Global Views',
|
||||
'db_table': 'global_views',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueactivity',
|
||||
name='epoch',
|
||||
field=models.FloatField(null=True),
|
||||
),
|
||||
migrations.RunPython(update_epoch),
|
||||
]
|
@ -50,8 +50,6 @@ export const BoardHeader: React.FC<Props> = ({
|
||||
|
||||
const { displayFilters, groupedIssues } = viewProps;
|
||||
|
||||
console.log("dF", displayFilters);
|
||||
|
||||
const { data: issueLabels } = useSWR(
|
||||
workspaceSlug && projectId && displayFilters?.group_by === "labels"
|
||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
||||
|
@ -13,19 +13,14 @@ import {
|
||||
} from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { MembersSelect, LabelSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, Tooltip } from "components/ui";
|
||||
// icons
|
||||
@ -44,7 +39,15 @@ import { LayerDiagonalIcon } from "components/icons";
|
||||
import { handleIssuesMutation } from "constants/issue";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, IIssueViewProps, ISubIssueResponse, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
import { CYCLE_DETAILS, MODULE_DETAILS, SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
@ -188,6 +191,86 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (snapshot.isDragging) handleTrashBox(snapshot.isDragging);
|
||||
}, [snapshot, handleTrashBox]);
|
||||
@ -343,13 +426,12 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-left break-words line-clamp-2"
|
||||
onClick={() => {
|
||||
if (isDraftIssue && handleDraftIssueEdit) handleDraftIssueEdit();
|
||||
else openPeekOverview();
|
||||
}}
|
||||
>
|
||||
{issue.name}
|
||||
<span className="text-sm text-left break-words line-clamp-2">{issue.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -359,21 +441,19 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
selfPositioned
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -397,16 +477,22 @@ export const SingleBoardIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={2} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
isNotAllowed={isNotAllowed}
|
||||
customButton
|
||||
user={user}
|
||||
selfPositioned
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -8,28 +8,23 @@ import { mutate } from "swr";
|
||||
import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||
import useIssuesProperties from "hooks/use-issue-properties";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CustomMenu, Tooltip } from "components/ui";
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewLabelSelect,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// icons
|
||||
import { LinkIcon, PaperClipIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { LayerDiagonalIcon } from "components/icons";
|
||||
// helper
|
||||
import { copyTextToClipboard, truncateText } from "helpers/string.helper";
|
||||
// type
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse } from "types";
|
||||
import { ICurrentUserResponse, IIssue, IState, ISubIssueResponse, TIssuePriorities } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
@ -153,6 +148,86 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const displayProperties = properties
|
||||
? Object.values(properties).some((value) => value === true)
|
||||
: false;
|
||||
@ -225,22 +300,19 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
{displayProperties && (
|
||||
<div className="relative mt-1.5 w-full flex flex-wrap items-center gap-2 text-xs">
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
isNotAllowed={isNotAllowed}
|
||||
user={user}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -260,21 +332,23 @@ export const SingleCalendarIssue: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{properties.labels && issue.labels.length > 0 && (
|
||||
<ViewLabelSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@ -87,8 +87,16 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { groupedByIssues, mutateIssues, displayFilters, filters, isEmpty, setFilters, params } =
|
||||
useIssuesView();
|
||||
const {
|
||||
groupedByIssues,
|
||||
mutateIssues,
|
||||
displayFilters,
|
||||
filters,
|
||||
isEmpty,
|
||||
setFilters,
|
||||
params,
|
||||
setDisplayFilters,
|
||||
} = useIssuesView();
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
@ -108,6 +116,17 @@ export const IssuesView: React.FC<Props> = ({
|
||||
|
||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDraftIssues) return;
|
||||
|
||||
if (
|
||||
displayFilters.layout === "calendar" ||
|
||||
displayFilters.layout === "gantt_chart" ||
|
||||
displayFilters.layout === "spreadsheet"
|
||||
)
|
||||
setDisplayFilters({ layout: "list" });
|
||||
}, [isDraftIssues, displayFilters, setDisplayFilters]);
|
||||
|
||||
const handleDeleteIssue = useCallback(
|
||||
(issue: IIssue) => {
|
||||
setDeleteIssueModal(true);
|
||||
|
@ -6,19 +6,13 @@ import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
CreateUpdateDraftIssueModal,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// ui
|
||||
import { Tooltip, CustomMenu, ContextMenu } from "components/ui";
|
||||
// icons
|
||||
@ -40,8 +34,10 @@ import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IIssueViewProps,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
IUserProfileProjectSegregation,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// fetch-keys
|
||||
@ -181,6 +177,86 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const issuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${issue.project}/archived-issues/${issue.id}`
|
||||
: isDraftIssues
|
||||
@ -290,21 +366,19 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.state && (
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.start_date && issue.start_date && (
|
||||
@ -323,14 +397,24 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
isNotAllowed={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.labels && <ViewIssueLabel labelDetails={issue.label_details} maxRender={3} />}
|
||||
{properties.assignee && (
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="right"
|
||||
{properties.labels && (
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={3}
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
)}
|
||||
{properties.estimate && issue.estimate_point !== null && (
|
||||
|
@ -5,15 +5,9 @@ import { useRouter } from "next/router";
|
||||
import { mutate } from "swr";
|
||||
|
||||
// components
|
||||
import {
|
||||
ViewAssigneeSelect,
|
||||
ViewDueDateSelect,
|
||||
ViewEstimateSelect,
|
||||
ViewIssueLabel,
|
||||
ViewPrioritySelect,
|
||||
ViewStartDateSelect,
|
||||
ViewStateSelect,
|
||||
} from "components/issues";
|
||||
import { ViewDueDateSelect, ViewEstimateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { LabelSelect, MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
import { Popover2 } from "@blueprintjs/popover2";
|
||||
// icons
|
||||
import { Icon } from "components/ui";
|
||||
@ -28,6 +22,7 @@ import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// constant
|
||||
import {
|
||||
CYCLE_DETAILS,
|
||||
@ -39,7 +34,15 @@ import {
|
||||
VIEW_ISSUES,
|
||||
} from "constants/fetch-keys";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue, ISubIssueResponse, Properties, UserAuth } from "types";
|
||||
import {
|
||||
ICurrentUserResponse,
|
||||
IIssue,
|
||||
IState,
|
||||
ISubIssueResponse,
|
||||
Properties,
|
||||
TIssuePriorities,
|
||||
UserAuth,
|
||||
} from "types";
|
||||
// helper
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
import { renderLongDetailDateFormat } from "helpers/date-time.helper";
|
||||
@ -180,6 +183,86 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (data: string, states: IState[] | undefined) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePriorityChange = (data: TIssuePriorities) => {
|
||||
partialUpdateIssue({ priority: data }, issue);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_PRIORITY",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleAssigneeChange = (data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
|
||||
if (newData.includes(data)) newData.splice(newData.indexOf(data), 1);
|
||||
else newData.push(data);
|
||||
|
||||
partialUpdateIssue({ assignees_list: data }, issue);
|
||||
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_ASSIGNEE",
|
||||
user
|
||||
);
|
||||
};
|
||||
|
||||
const handleLabelChange = (data: any) => {
|
||||
partialUpdateIssue({ labels_list: data }, issue);
|
||||
};
|
||||
|
||||
const paddingLeft = `${nestingLevel * 68}px`;
|
||||
|
||||
const tooltipPosition = index === 0 ? "bottom" : "top";
|
||||
@ -283,47 +366,49 @@ export const SingleSpreadsheetIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
{properties.state && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewStateSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
className="max-w-full"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
onChange={handleStateChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.priority && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
noBorder
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<PrioritySelect
|
||||
value={issue.priority}
|
||||
onChange={handlePriorityChange}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewAssigneeSelect
|
||||
issue={issue}
|
||||
partialUpdateIssue={partialUpdateIssue}
|
||||
position="left"
|
||||
tooltipPosition={tooltipPosition}
|
||||
customButton
|
||||
user={user}
|
||||
isNotAllowed={isNotAllowed}
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
onChange={handleAssigneeChange}
|
||||
membersDetails={issue.assignee_details}
|
||||
buttonClassName="!p-0 !rounded-none !shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{properties.labels && (
|
||||
<div className="flex items-center text-xs text-custom-text-200 text-center p-2 group-hover:bg-custom-background-80 border-custom-border-200">
|
||||
<ViewIssueLabel labelDetails={issue.label_details} maxRender={1} />
|
||||
<LabelSelect
|
||||
value={issue.labels}
|
||||
onChange={handleLabelChange}
|
||||
labelsDetails={issue.label_details}
|
||||
hideDropdownArrow
|
||||
maxRender={1}
|
||||
user={user}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -127,7 +127,7 @@ export const ActiveCycleDetails: React.FC = () => {
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
@ -221,7 +221,7 @@ export const CyclesView: React.FC<Props> = ({ cycles, mutateCycles, viewType })
|
||||
cy="34.375"
|
||||
r="22"
|
||||
stroke="rgb(var(--color-text-400))"
|
||||
stroke-linecap="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M36.4375 20.9919C36.4375 19.2528 37.6796 17.8127 39.1709 18.1419C40.125 18.3526 41.0604 18.6735 41.9625 19.1014C43.7141 19.9322 45.3057 21.1499 46.6464 22.685C47.987 24.2202 49.0505 26.0426 49.776 28.0484C50.5016 30.0541 50.875 32.2038 50.875 34.3748C50.875 36.5458 50.5016 38.6956 49.776 40.7013C49.0505 42.7071 47.987 44.5295 46.6464 46.0647C45.3057 47.5998 43.7141 48.8175 41.9625 49.6483C41.0604 50.0762 40.125 50.3971 39.1709 50.6077C37.6796 50.937 36.4375 49.4969 36.4375 47.7578L36.4375 20.9919Z"
|
||||
|
@ -20,7 +20,7 @@ export const ModuleCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="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="#ef4444"
|
||||
|
@ -16,7 +16,7 @@ export const ModulePausedIcon: React.FC<Props> = ({ width = "20", height = "20",
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100275)">
|
||||
<g clipPath="url(#clip0_4052_100275)">
|
||||
<path
|
||||
d="M6.4435 10.34C6.6145 10.34 6.75667 10.2825 6.87 10.1675C6.98333 10.0525 7.04 9.91 7.04 9.74V6.24C7.04 6.07 6.98217 5.9275 6.8665 5.8125C6.75082 5.6975 6.60749 5.64 6.4365 5.64C6.2655 5.64 6.12333 5.6975 6.01 5.8125C5.89667 5.9275 5.84 6.07 5.84 6.24V9.74C5.84 9.91 5.89783 10.0525 6.0135 10.1675C6.12918 10.2825 6.27251 10.34 6.4435 10.34ZM9.5635 10.34C9.7345 10.34 9.87667 10.2825 9.99 10.1675C10.1033 10.0525 10.16 9.91 10.16 9.74V6.24C10.16 6.07 10.1022 5.9275 9.9865 5.8125C9.87082 5.6975 9.72749 5.64 9.5565 5.64C9.3855 5.64 9.24333 5.6975 9.13 5.8125C9.01667 5.9275 8.96 6.07 8.96 6.24V9.74C8.96 9.91 9.01783 10.0525 9.1335 10.1675C9.24918 10.2825 9.39251 10.34 9.5635 10.34ZM8 16C6.89333 16 5.85333 15.79 4.88 15.37C3.90667 14.95 3.06 14.38 2.34 13.66C1.62 12.94 1.05 12.0933 0.63 11.12C0.21 10.1467 0 9.10667 0 8C0 7.54667 0.0366667 7.09993 0.11 6.6598C0.183333 6.21965 0.293333 5.78639 0.44 5.36C0.493333 5.21333 0.593333 5.11667 0.74 5.07C0.886667 5.02333 1.02667 5.04199 1.16 5.12596C1.30285 5.20993 1.40523 5.33327 1.46714 5.49596C1.52905 5.65865 1.54 5.82 1.5 5.98C1.42 6.31333 1.35 6.64765 1.29 6.98294C1.23 7.31823 1.2 7.65725 1.2 8C1.2 9.89833 1.85875 11.5063 3.17624 12.8238C4.49375 14.1413 6.10167 14.8 8 14.8C9.89833 14.8 11.5063 14.1413 12.8238 12.8238C14.1413 11.5063 14.8 9.89833 14.8 8C14.8 6.10167 14.1413 4.49375 12.8238 3.17624C11.5063 1.85875 9.89833 1.2 8 1.2C7.63235 1.2 7.26852 1.22667 6.90852 1.28C6.54852 1.33333 6.19235 1.41333 5.84 1.52C5.68 1.57333 5.52 1.56667 5.36 1.5C5.2 1.43333 5.08667 1.32667 5.02 1.18C4.95333 1.03333 4.96 0.886667 5.04 0.74C5.12 0.593333 5.23333 0.493333 5.38 0.44C5.79333 0.306667 6.21333 0.2 6.64 0.12C7.06667 0.04 7.49333 0 7.92 0C9.02667 0 10.07 0.21 11.05 0.63C12.03 1.05 12.8863 1.62 13.6189 2.34C14.3516 3.06 14.9316 3.90667 15.3589 4.88C15.7863 5.85333 16 6.89333 16 8C16 9.10667 15.79 10.1467 15.37 11.12C14.95 12.0933 14.38 12.94 13.66 13.66C12.94 14.38 12.0933 14.95 11.12 15.37C10.1467 15.79 9.10667 16 8 16ZM2.65764 3.62C2.37921 3.62 2.14333 3.52255 1.95 3.32764C1.75667 3.13275 1.66 2.89608 1.66 2.61764C1.66 2.33921 1.75745 2.10333 1.95236 1.91C2.14725 1.71667 2.38392 1.62 2.66236 1.62C2.94079 1.62 3.17667 1.71745 3.37 1.91236C3.56333 2.10725 3.66 2.34392 3.66 2.62236C3.66 2.90079 3.56255 3.13667 3.36764 3.33C3.17275 3.52333 2.93608 3.62 2.65764 3.62Z"
|
||||
fill="#525252"
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupBacklogIcon: React.FC<Props> = ({
|
||||
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" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" strokeDasharray="4 4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupCancelledIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_4052_100277)">
|
||||
<g clipPath="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}
|
||||
|
@ -19,7 +19,7 @@ export const StateGroupStartedIcon: React.FC<Props> = ({
|
||||
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" />
|
||||
<circle cx="6" cy="6" r="5.6" stroke={color} strokeWidth="0.8" />
|
||||
<circle cx="6" cy="6" r="3.35" stroke={color} strokeWidth="0.8" strokeDasharray="2.4 2.4" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -19,6 +19,6 @@ export const StateGroupUnstartedIcon: React.FC<Props> = ({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} stroke-width="1.2" />
|
||||
<circle cx="8" cy="8" r="7.4" stroke={color} strokeWidth="1.2" />
|
||||
</svg>
|
||||
);
|
||||
|
@ -57,7 +57,7 @@ export const ConfirmIssueDiscard: React.FC<Props> = (props) => {
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-[40rem]">
|
||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
|
@ -55,7 +55,10 @@ const defaultValues: Partial<IIssue> = {
|
||||
};
|
||||
|
||||
interface IssueFormProps {
|
||||
handleFormSubmit: (formData: Partial<IIssue>) => Promise<void>;
|
||||
handleFormSubmit: (
|
||||
formData: Partial<IIssue>,
|
||||
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
|
||||
) => Promise<void>;
|
||||
data?: Partial<IIssue> | null;
|
||||
prePopulatedData?: Partial<IIssue> | null;
|
||||
projectId: string;
|
||||
@ -134,12 +137,16 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
|
||||
const handleCreateUpdateIssue = async (
|
||||
formData: Partial<IIssue>,
|
||||
action: "saveDraft" | "createToNewIssue" = "saveDraft"
|
||||
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
|
||||
) => {
|
||||
await handleFormSubmit({
|
||||
...formData,
|
||||
is_draft: action === "saveDraft",
|
||||
});
|
||||
await handleFormSubmit(
|
||||
{
|
||||
...(data ?? {}),
|
||||
...formData,
|
||||
is_draft: action === "createDraft" || action === "updateDraft",
|
||||
},
|
||||
action
|
||||
);
|
||||
|
||||
setGptAssistantModal(false);
|
||||
|
||||
@ -263,7 +270,9 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
</>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit((formData) => handleCreateUpdateIssue(formData, "createToNewIssue"))}
|
||||
onSubmit={handleSubmit((formData) =>
|
||||
handleCreateUpdateIssue(formData, "convertToNewIssue")
|
||||
)}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-x-2">
|
||||
@ -563,15 +572,20 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
<SecondaryButton onClick={onClose}>Discard</SecondaryButton>
|
||||
<SecondaryButton
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((formData) => handleCreateUpdateIssue(formData, "saveDraft"))}
|
||||
onClick={handleSubmit((formData) =>
|
||||
handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft")
|
||||
)}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save Draft"}
|
||||
</SecondaryButton>
|
||||
{data && (
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Saving..." : "Add Issue"}
|
||||
</PrimaryButton>
|
||||
)}
|
||||
<PrimaryButton
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((formData) =>
|
||||
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createNewIssue")
|
||||
)}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Add Issue"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -31,7 +31,10 @@ import {
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
VIEW_ISSUES,
|
||||
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
} from "constants/fetch-keys";
|
||||
import modulesService from "services/modules.service";
|
||||
|
||||
interface IssuesModalProps {
|
||||
data?: IIssue | null;
|
||||
@ -56,18 +59,21 @@ interface IssuesModalProps {
|
||||
onSubmit?: (data: Partial<IIssue>) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
data,
|
||||
handleClose,
|
||||
isOpen,
|
||||
isUpdatingSingleIssue = false,
|
||||
prePopulateData,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
}) => {
|
||||
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) => {
|
||||
const {
|
||||
data,
|
||||
handleClose,
|
||||
isOpen,
|
||||
isUpdatingSingleIssue = false,
|
||||
prePopulateData: prePopulateDataProps,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
} = props;
|
||||
|
||||
// states
|
||||
const [createMore, setCreateMore] = useState(false);
|
||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
@ -86,19 +92,40 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
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") || router.asPath.includes("assigned"))
|
||||
prePopulateData = {
|
||||
...prePopulateData,
|
||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
setActiveProject(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPreloadedData(prePopulateDataProps ?? {});
|
||||
|
||||
if (cycleId && !prePopulateDataProps?.cycle) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
cycle: cycleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (moduleId && !prePopulateDataProps?.module) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
module: moduleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (
|
||||
(router.asPath.includes("my-issues") || router.asPath.includes("assigned")) &&
|
||||
!prePopulateDataProps?.assignees
|
||||
) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""],
|
||||
}));
|
||||
}
|
||||
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
// if modal is closed, reset active project to null
|
||||
// and return to avoid activeProject being set to some other project
|
||||
@ -109,10 +136,10 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
// if data is present, set active project to the project of the
|
||||
// issue. This has more priority than the project in the url.
|
||||
if (data && data.project) {
|
||||
setActiveProject(data.project);
|
||||
return;
|
||||
}
|
||||
if (data && data.project) return setActiveProject(data.project);
|
||||
|
||||
if (prePopulateData && prePopulateData.project && !activeProject)
|
||||
return setActiveProject(prePopulateData.project);
|
||||
|
||||
if (prePopulateData && prePopulateData.project)
|
||||
return setActiveProject(prePopulateData.project);
|
||||
@ -147,7 +174,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
|
||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
|
||||
|
||||
const createIssue = async (payload: Partial<IIssue>) => {
|
||||
const createDraftIssue = async (payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !activeProject || !user) return;
|
||||
|
||||
await issuesService
|
||||
@ -187,7 +214,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (!createMore) onClose();
|
||||
};
|
||||
|
||||
const updateIssue = async (payload: Partial<IIssue>) => {
|
||||
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
||||
if (!user) return;
|
||||
|
||||
await issuesService
|
||||
@ -203,6 +230,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
}
|
||||
|
||||
if (!payload.is_draft) {
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||
}
|
||||
|
||||
if (!createMore) onClose();
|
||||
|
||||
setToastAlert({
|
||||
@ -220,7 +252,93 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (formData: Partial<IIssue>) => {
|
||||
const addIssueToCycle = async (issueId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
await issuesService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
activeProject ?? "",
|
||||
cycleId,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (cycleId) {
|
||||
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
|
||||
mutate(CYCLE_DETAILS(cycleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addIssueToModule = async (issueId: string, moduleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
await modulesService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
activeProject ?? "",
|
||||
moduleId as string,
|
||||
{
|
||||
issues: [issueId],
|
||||
},
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
if (moduleId) {
|
||||
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||
mutate(MODULE_DETAILS(moduleId as string));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createIssue = async (payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
await issuesService
|
||||
.createIssues(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||
.then(async (res) => {
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
||||
|
||||
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||
if (displayFilters.layout === "gantt_chart")
|
||||
mutate(ganttFetchKey, {
|
||||
start_target_date: true,
|
||||
order_by: "sort_order",
|
||||
});
|
||||
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
if (!createMore) onClose();
|
||||
|
||||
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));
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be created. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (
|
||||
formData: Partial<IIssue>,
|
||||
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
|
||||
) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
const payload: Partial<IIssue> = {
|
||||
@ -231,8 +349,10 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = ({
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
};
|
||||
|
||||
if (!data) await createIssue(payload);
|
||||
else await updateIssue(payload);
|
||||
if (action === "createDraft") await createDraftIssue(payload);
|
||||
else if (action === "updateDraft" || action === "convertToNewIssue")
|
||||
await updateDraftIssue(payload);
|
||||
else if (action === "createNewIssue") await createIssue(payload);
|
||||
|
||||
clearDraftIssueLocalStorage();
|
||||
|
||||
|
@ -139,6 +139,8 @@ export const IssueForm: FC<IssueFormProps> = (props) => {
|
||||
target_date: getValues("target_date"),
|
||||
project: getValues("project"),
|
||||
parent: getValues("parent"),
|
||||
cycle: getValues("cycle"),
|
||||
module: getValues("module"),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -69,7 +69,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
handleClose,
|
||||
isOpen,
|
||||
isUpdatingSingleIssue = false,
|
||||
prePopulateData,
|
||||
prePopulateData: prePopulateDataProps,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
}) => {
|
||||
@ -78,6 +78,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
const [formDirtyState, setFormDirtyState] = useState<any>(null);
|
||||
const [showConfirmDiscard, setShowConfirmDiscard] = useState(false);
|
||||
const [activeProject, setActiveProject] = useState<string | null>(null);
|
||||
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue>>({});
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
|
||||
@ -98,11 +99,40 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
if (router.asPath.includes("my-issues") || router.asPath.includes("assigned"))
|
||||
prePopulateData = {
|
||||
...prePopulateData,
|
||||
assignees: [...(prePopulateData?.assignees ?? []), user?.id ?? ""],
|
||||
};
|
||||
useEffect(() => {
|
||||
setPreloadedData(prePopulateDataProps ?? {});
|
||||
|
||||
if (cycleId && !prePopulateDataProps?.cycle) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
cycle: cycleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (moduleId && !prePopulateDataProps?.module) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
module: moduleId.toString(),
|
||||
}));
|
||||
}
|
||||
if (
|
||||
(router.asPath.includes("my-issues") || router.asPath.includes("assigned")) &&
|
||||
!prePopulateDataProps?.assignees
|
||||
) {
|
||||
setPreloadedData((prevData) => ({
|
||||
...(prevData ?? {}),
|
||||
...prePopulateDataProps,
|
||||
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""],
|
||||
}));
|
||||
}
|
||||
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @description This function is used to close the modals. This function will show a confirm discard modal if the form is dirty.
|
||||
* @returns void
|
||||
*/
|
||||
|
||||
const onClose = () => {
|
||||
if (!showConfirmDiscard) handleClose();
|
||||
@ -111,6 +141,22 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
setValueInLocalStorage(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This function is used to close the modals. This function is to be used when the form is submitted,
|
||||
* meaning we don't need to show the confirm discard modal or store the form data in local storage.
|
||||
*/
|
||||
|
||||
const onFormSubmitClose = () => {
|
||||
setFormDirtyState(null);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This function is used to close the modals. This function is to be used when we click outside the modal,
|
||||
* meaning we don't need to show the confirm discard modal but will store the form data in local storage.
|
||||
* Use this function when you want to store the form data in local storage.
|
||||
*/
|
||||
|
||||
const onDiscardClose = () => {
|
||||
if (formDirtyState !== null) {
|
||||
setShowConfirmDiscard(true);
|
||||
@ -295,7 +341,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
if (!createMore) onDiscardClose();
|
||||
if (!createMore) onFormSubmitClose();
|
||||
};
|
||||
|
||||
const createDraftIssue = async () => {
|
||||
@ -354,7 +400,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||
|
||||
if (!createMore) onDiscardClose();
|
||||
if (!createMore) onFormSubmitClose();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
|
@ -3,5 +3,4 @@ export * from "./due-date";
|
||||
export * from "./estimate";
|
||||
export * from "./label";
|
||||
export * from "./priority";
|
||||
export * from "./start-date";
|
||||
export * from "./state";
|
||||
export * from "./start-date";
|
@ -1,138 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
import trackEventServices from "services/track-event.service";
|
||||
// ui
|
||||
import { CustomSearchSelect, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { ICurrentUserResponse, IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issue: IIssue;
|
||||
partialUpdateIssue: (formData: Partial<IIssue>, issue: IIssue) => void;
|
||||
position?: "left" | "right";
|
||||
tooltipPosition?: "top" | "bottom";
|
||||
className?: string;
|
||||
selfPositioned?: boolean;
|
||||
customButton?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
isNotAllowed: boolean;
|
||||
};
|
||||
|
||||
export const ViewStateSelect: React.FC<Props> = ({
|
||||
issue,
|
||||
partialUpdateIssue,
|
||||
position = "left",
|
||||
tooltipPosition = "top",
|
||||
className = "",
|
||||
selfPositioned = false,
|
||||
customButton = false,
|
||||
user,
|
||||
isNotAllowed,
|
||||
}) => {
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && issue && fetchStates ? STATES_LIST(issue.project) : null,
|
||||
workspaceSlug && issue && fetchStates
|
||||
? () => stateService.getStates(workspaceSlug as string, issue.project)
|
||||
: null
|
||||
);
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
const options = states?.map((state) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const selectedOption = issue.state_detail;
|
||||
|
||||
const stateLabel = (
|
||||
<Tooltip
|
||||
tooltipHeading="State"
|
||||
tooltipContent={selectedOption?.name ?? ""}
|
||||
position={tooltipPosition}
|
||||
>
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<span className="h-3.5 w-3.5">
|
||||
{selectedOption && (
|
||||
<StateGroupIcon stateGroup={selectedOption.group} color={selectedOption.color} />
|
||||
)}
|
||||
</span>
|
||||
<span className="truncate">{selectedOption?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomSearchSelect
|
||||
className={className}
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
const oldState = states?.find((s) => s.id === issue.state);
|
||||
const newState = states?.find((s) => s.id === data);
|
||||
|
||||
partialUpdateIssue(
|
||||
{
|
||||
state: data,
|
||||
state_detail: newState,
|
||||
},
|
||||
issue
|
||||
);
|
||||
trackEventServices.trackIssuePartialPropertyUpdateEvent(
|
||||
{
|
||||
workspaceSlug,
|
||||
workspaceId: issue.workspace,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
"ISSUE_PROPERTY_UPDATE_STATE",
|
||||
user
|
||||
);
|
||||
|
||||
if (oldState?.group !== "completed" && newState?.group !== "completed") {
|
||||
trackEventServices.trackIssueMarkedAsDoneEvent(
|
||||
{
|
||||
workspaceSlug: issue.workspace_detail.slug,
|
||||
workspaceId: issue.workspace_detail.id,
|
||||
projectId: issue.project_detail.id,
|
||||
projectIdentifier: issue.project_detail.identifier,
|
||||
projectName: issue.project_detail.name,
|
||||
issueId: issue.id,
|
||||
},
|
||||
user
|
||||
);
|
||||
}
|
||||
}}
|
||||
options={options}
|
||||
{...(customButton ? { customButton: stateLabel } : { label: stateLabel })}
|
||||
position={position}
|
||||
disabled={isNotAllowed}
|
||||
onOpen={() => setFetchStates(true)}
|
||||
noChevron
|
||||
selfPositioned={selfPositioned}
|
||||
/>
|
||||
);
|
||||
};
|
@ -7,3 +7,6 @@ export * from "./single-project-card";
|
||||
export * from "./single-sidebar-project";
|
||||
export * from "./confirm-project-leave-modal";
|
||||
export * from "./member-select";
|
||||
export * from "./members-select";
|
||||
export * from "./label-select";
|
||||
export * from "./priority-select";
|
||||
|
243
web/components/project/label-select.tsx
Normal file
243
web/components/project/label-select.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// component
|
||||
import { CreateLabelModal } from "components/labels";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// types
|
||||
import { Tooltip } from "components/ui";
|
||||
import { ICurrentUserResponse, IIssueLabels } from "types";
|
||||
// constants
|
||||
import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (data: any) => void;
|
||||
labelsDetails: any[];
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
maxRender?: number;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
};
|
||||
|
||||
export const LabelSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
labelsDetails,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
maxRender = 2,
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
user,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const [labelModal, setLabelModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const { data: issueLabels } = useSWR<IIssueLabels[]>(
|
||||
projectId && fetchStates ? PROJECT_ISSUE_LABELS(projectId.toString()) : null,
|
||||
workspaceSlug && projectId && fetchStates
|
||||
? () => 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 filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<div className={`flex items-center gap-2 text-custom-text-200`}>
|
||||
{labelsDetails.length > 0 ? (
|
||||
labelsDetails.length <= maxRender ? (
|
||||
<>
|
||||
{labelsDetails.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color ?? "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex cursor-default items-center flex-shrink-0 rounded-md border border-custom-border-300 px-2.5 py-1 text-xs shadow-sm">
|
||||
<Tooltip
|
||||
position="top"
|
||||
tooltipHeading="Labels"
|
||||
tooltipContent={labelsDetails.map((l) => l.name).join(", ")}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||
{`${value.length} Labels`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
const footerOption = (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setLabelModal(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
{footerOption}
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
</>
|
||||
);
|
||||
};
|
191
web/components/project/members-select.tsx
Normal file
191
web/components/project/members-select.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
import useProjectMembers from "hooks/use-project-members";
|
||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// components
|
||||
import { AssigneesList, Avatar, Icon, Tooltip } from "components/ui";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IUser } from "types";
|
||||
|
||||
type Props = {
|
||||
value: string | string[];
|
||||
onChange: (data: any) => void;
|
||||
membersDetails: IUser[];
|
||||
renderWorkspaceMembers?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const MembersSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
membersDetails,
|
||||
renderWorkspaceMembers = false,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [fetchStates, setFetchStates] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const { members } = useProjectMembers(
|
||||
workspaceSlug?.toString(),
|
||||
projectId?.toString(),
|
||||
fetchStates && !renderWorkspaceMembers
|
||||
);
|
||||
|
||||
const { workspaceMembers } = useWorkspaceMembers(
|
||||
workspaceSlug?.toString() ?? "",
|
||||
fetchStates && renderWorkspaceMembers
|
||||
);
|
||||
|
||||
const membersOptions = renderWorkspaceMembers ? workspaceMembers : members;
|
||||
|
||||
const options = membersOptions?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip
|
||||
tooltipHeading="Assignee"
|
||||
tooltipContent={
|
||||
membersDetails.length > 0
|
||||
? membersDetails.map((assignee) => assignee?.display_name).join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<AssigneesList userIds={value} length={3} showLength={true} />
|
||||
) : (
|
||||
<span
|
||||
className="flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none
|
||||
"
|
||||
>
|
||||
<Icon iconName="person" className="text-sm !leading-4" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
multiple
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
173
web/components/project/priority-select.tsx
Normal file
173
web/components/project/priority-select.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { PriorityIcon } from "components/icons";
|
||||
// components
|
||||
import { Tooltip } from "components/ui";
|
||||
// types
|
||||
import { TIssuePriorities } from "types";
|
||||
// constants
|
||||
import { PRIORITIES } from "constants/project";
|
||||
|
||||
type Props = {
|
||||
value: TIssuePriorities;
|
||||
onChange: (data: any) => void;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const PrioritySelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const options = PRIORITIES?.map((priority) => ({
|
||||
value: priority,
|
||||
query: priority,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<PriorityIcon priority={priority} className="text-sm" />
|
||||
{priority ?? "None"}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const selectedOption = value ?? "None";
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={selectedOption} position="top">
|
||||
<div
|
||||
className={`grid place-items-center rounded "h-6 w-6 border shadow-sm ${
|
||||
value === "urgent"
|
||||
? "border-red-500/20 bg-red-500"
|
||||
: "border-custom-border-300 bg-custom-background-100"
|
||||
} items-center`}
|
||||
>
|
||||
<span className="flex gap-1 items-center text-custom-text-200 text-xs">
|
||||
<PriorityIcon
|
||||
priority={value}
|
||||
className={`text-sm ${
|
||||
value === "urgent"
|
||||
? "text-white"
|
||||
: value === "high"
|
||||
? "text-orange-500"
|
||||
: value === "medium"
|
||||
? "text-yellow-500"
|
||||
: value === "low"
|
||||
? "text-green-500"
|
||||
: "text-custom-text-200"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
@ -2,3 +2,4 @@ export * from "./create-update-state-inline";
|
||||
export * from "./create-state-modal";
|
||||
export * from "./delete-state-modal";
|
||||
export * from "./single-state";
|
||||
export * from "./state-select";
|
||||
|
177
web/components/states/state-select.tsx
Normal file
177
web/components/states/state-select.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// hooks
|
||||
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// headless ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { CheckIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { StateGroupIcon } from "components/icons";
|
||||
// types
|
||||
import { Tooltip } from "components/ui";
|
||||
// constants
|
||||
import { IState } from "types";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
// helper
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
|
||||
type Props = {
|
||||
value: IState;
|
||||
onChange: (data: any, states: IState[] | undefined) => void;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
hideDropdownArrow?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const StateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
hideDropdownArrow = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const dropdownBtn = useRef<any>(null);
|
||||
const dropdownOptions = useRef<any>(null);
|
||||
|
||||
const [fetchStates, setFetchStates] = useState<boolean>(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId && fetchStates ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId && fetchStates
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const states = getStatesList(stateGroups);
|
||||
|
||||
const options = states?.map((state) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? options
|
||||
: options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={value?.name ?? ""} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<span className="h-3.5 w-3.5">
|
||||
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />}
|
||||
</span>
|
||||
<span className="truncate">{value?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value.id}
|
||||
onChange={(data: string) => {
|
||||
onChange(data, states);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{({ open }: { open: boolean }) => {
|
||||
if (open) {
|
||||
if (!isOpen) setIsOpen(true);
|
||||
setFetchStates(true);
|
||||
} else if (isOpen) setIsOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox.Button
|
||||
ref={dropdownBtn}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded-md shadow-sm border border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</Combobox.Button>
|
||||
<div className={`${open ? "fixed z-20 top-0 left-0 h-full w-full cursor-auto" : ""}`}>
|
||||
<Combobox.Options
|
||||
ref={dropdownOptions}
|
||||
className={`absolute z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-lg focus:outline-none w-48 whitespace-nowrap mt-1 ${optionsClassName}`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <CheckIcon className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Combobox>
|
||||
);
|
||||
};
|
@ -3,8 +3,6 @@ import React, { useState } from "react";
|
||||
// ui
|
||||
import { Icon } from "components/ui";
|
||||
import { ChevronDown, PenSquare } from "lucide-react";
|
||||
// headless ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
// components
|
||||
@ -17,10 +15,7 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
|
||||
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||
|
||||
const { storedValue, clearValue } = useLocalStorage<any>(
|
||||
"draftedIssue",
|
||||
JSON.stringify(undefined)
|
||||
);
|
||||
const { storedValue, clearValue } = useLocalStorage<any>("draftedIssue", JSON.stringify({}));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -31,18 +26,17 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
onSubmit={() => {
|
||||
localStorage.removeItem("draftedIssue");
|
||||
clearValue();
|
||||
setIsDraftIssueModalOpen(false);
|
||||
}}
|
||||
fieldsToShow={["all"]}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`relative flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
||||
className={`flex items-center justify-between w-full cursor-pointer px-4 mt-4 ${
|
||||
store?.theme?.sidebarCollapsed ? "flex-col gap-1" : "gap-2"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
|
||||
className={`relative flex items-center justify-between w-full rounded cursor-pointer px-2 gap-1 group ${
|
||||
store?.theme?.sidebarCollapsed
|
||||
? "px-2 hover:bg-custom-sidebar-background-80"
|
||||
: "px-3 shadow border-[0.5px] border-custom-border-300"
|
||||
@ -50,7 +44,7 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
|
||||
className="relative flex items-center gap-2 flex-grow rounded flex-shrink-0 py-1.5"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
||||
document.dispatchEvent(e);
|
||||
@ -65,56 +59,35 @@ export const WorkspaceSidebarQuickAction = () => {
|
||||
)}
|
||||
</button>
|
||||
|
||||
{storedValue && <div className="h-8 w-0.5 bg-custom-sidebar-background-80" />}
|
||||
{storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && (
|
||||
<>
|
||||
<div className="h-8 w-0.5 bg-custom-sidebar-background-80" />
|
||||
|
||||
{storedValue && (
|
||||
<div className="relative">
|
||||
<Menu as={React.Fragment}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Menu.Button
|
||||
type="button"
|
||||
className={`flex items-center justify-center rounded flex-shrink-0 p-1.5 ${
|
||||
open ? "rotate-180 pl-0" : "rotate-0 pr-0"
|
||||
}`}
|
||||
>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="!text-custom-sidebar-text-300 transform transition-transform duration-300"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={React.Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -right-4 mt-1 w-52 bg-custom-background-300">
|
||||
<div className="px-1 py-1 ">
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
|
||||
>
|
||||
<PenSquare
|
||||
size={16}
|
||||
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
|
||||
/>
|
||||
Last Drafted Issue
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-center rounded flex-shrink-0 py-1.5 ml-1.5"
|
||||
>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="!text-custom-sidebar-text-300 transform transition-transform duration-300 group-hover:rotate-180 rotate-0"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div className="absolute w-full h-10 pt-2 top-full left-0 opacity-0 group-hover:opacity-100 mt-0 pointer-events-none group-hover:pointer-events-auto">
|
||||
<div className="w-full h-full">
|
||||
<button
|
||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||
className="w-full flex text-sm items-center rounded flex-shrink-0 py-[10px] px-3 bg-custom-background-100 shadow border-[0.5px] border-custom-border-300 text-custom-text-300"
|
||||
>
|
||||
<PenSquare
|
||||
size={16}
|
||||
className="!text-lg !leading-4 text-custom-sidebar-text-300 mr-2"
|
||||
/>
|
||||
Last Drafted Issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
64
web/hooks/use-dynamic-dropdown.tsx
Normal file
64
web/hooks/use-dynamic-dropdown.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
// hook
|
||||
import useOutsideClickDetector from "./use-outside-click-detector";
|
||||
|
||||
/**
|
||||
* Custom hook for dynamic dropdown position calculation.
|
||||
* @param isOpen - Indicates whether the dropdown is open.
|
||||
* @param handleClose - Callback to handle closing the dropdown.
|
||||
* @param buttonRef - Ref object for the button triggering the dropdown.
|
||||
* @param dropdownRef - Ref object for the dropdown element.
|
||||
*/
|
||||
|
||||
const useDynamicDropdownPosition = (
|
||||
isOpen: boolean,
|
||||
handleClose: () => void,
|
||||
buttonRef: React.RefObject<any>,
|
||||
dropdownRef: React.RefObject<any>
|
||||
) => {
|
||||
const handlePosition = useCallback(() => {
|
||||
const button = buttonRef.current;
|
||||
const dropdown = dropdownRef.current;
|
||||
|
||||
if (!dropdown || !button) return;
|
||||
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const dropdownRect = dropdown.getBoundingClientRect();
|
||||
|
||||
const { innerHeight, innerWidth, scrollX, scrollY } = window;
|
||||
|
||||
let top: number = buttonRect.bottom + scrollY;
|
||||
if (top + dropdownRect.height > innerHeight) top = innerHeight - dropdownRect.height;
|
||||
|
||||
let left: number = buttonRect.left + scrollX + (buttonRect.width - dropdownRect.width) / 2;
|
||||
if (left + dropdownRect.width > innerWidth) left = innerWidth - dropdownRect.width;
|
||||
|
||||
dropdown.style.top = `${Math.max(top, 5)}px`;
|
||||
dropdown.style.left = `${Math.max(left, 5)}px`;
|
||||
}, [buttonRef, dropdownRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) handlePosition();
|
||||
}, [handlePosition, isOpen]);
|
||||
|
||||
useOutsideClickDetector(dropdownRef, () => {
|
||||
if (isOpen) handleClose();
|
||||
});
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
if (isOpen) {
|
||||
handlePosition();
|
||||
}
|
||||
}, [handlePosition, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [isOpen, handleResize]);
|
||||
};
|
||||
|
||||
export default useDynamicDropdownPosition;
|
Loading…
Reference in New Issue
Block a user