Compare commits

..
Author SHA1 Message Date
NarayanBavisetti e1145a7d38 chore: issue patch external api 2024-04-24 18:07:23 +05:30
26 changed files with 371 additions and 562 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ env:
jobs:
sync_changes:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
-6
View File
@@ -6,15 +6,9 @@ from plane.api.views import (
IssueLinkAPIEndpoint,
IssueCommentAPIEndpoint,
IssueActivityAPIEndpoint,
WorkspaceIssueAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/issues/<str:project__identifier>-<str:issue__identifier>/",
WorkspaceIssueAPIEndpoint.as_view(),
name="issue-by-identifier",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueAPIEndpoint.as_view(),
-1
View File
@@ -3,7 +3,6 @@ from .project import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
from .state import StateAPIEndpoint
from .issue import (
WorkspaceIssueAPIEndpoint,
IssueAPIEndpoint,
LabelAPIEndpoint,
IssueLinkAPIEndpoint,
+1 -61
View File
@@ -32,7 +32,6 @@ from plane.api.serializers import (
LabelSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
@@ -52,65 +51,6 @@ from plane.db.models import (
from .base import BaseAPIView, WebhookMixin
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset provides `retrieveByIssueId` on workspace level
"""
model = Issue
webhook_event = "issue"
permission_classes = [
ProjectEntityPermission
]
serializer_class = IssueSerializer
@property
def project__identifier(self):
return self.kwargs.get("project__identifier", None)
def get_queryset(self):
return (
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")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project__identifier=self.kwargs.get("project__identifier"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(self, request, slug, project__identifier=None, issue__identifier=None):
if issue__identifier and project__identifier:
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__identifier=project__identifier, sequence_id=issue__identifier)
return Response(
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
@@ -497,7 +437,7 @@ class LabelAPIEndpoint(BaseAPIView):
serializer = LabelSerializer(label, data=request.data, partial=True)
if serializer.is_valid():
if (
str(request.data.get("external_id"))
request.data.get("external_id")
and (label.external_id != str(request.data.get("external_id")))
and Issue.objects.filter(
project_id=project_id,
@@ -79,16 +79,6 @@ class ProjectEntityPermission(BasePermission):
if request.user.is_anonymous:
return False
# Handle requests based on project__identifier
if hasattr(view, "project__identifier") and view.project__identifier:
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug,
member=request.user,
project__identifier=view.project__identifier,
is_active=True,
).exists()
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
+2 -2
View File
@@ -151,8 +151,8 @@ class WorkSpaceViewSet(BaseViewSet):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True)
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -21,7 +21,6 @@ class WorkspaceStatesEndpoint(BaseAPIView):
project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True,
project__archived_at__isnull=True,
is_triage=False,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
@@ -6,11 +6,10 @@ export type TControlLink = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
children: React.ReactNode;
target?: string;
disabled?: boolean;
className?: string;
};
export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((props, ref) => {
const { href, onClick, children, target = "_self", disabled = false, className, ...rest } = props;
export const ControlLink: React.FC<TControlLink> = (props) => {
const { href, onClick, children, target = "_self", disabled = false, ...rest } = props;
const LEFT_CLICK_EVENT_CODE = 0;
const handleOnClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
@@ -24,8 +23,8 @@ export const ControlLink = React.forwardRef<HTMLAnchorElement, TControlLink>((pr
if (disabled) return <>{children}</>;
return (
<a href={href} target={target} onClick={handleOnClick} {...rest} ref={ref} className={className}>
<a href={href} target={target} onClick={handleOnClick} {...rest}>
{children}
</a>
);
});
};
@@ -36,7 +36,6 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
workspaceLevelToggle = false,
} = props;
// states
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
@@ -73,7 +72,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
useEffect(() => {
if (!isOpen || !workspaceSlug || !projectId) return;
setIsLoading(true);
projectService
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
search: debouncedSearchTerm,
@@ -81,10 +80,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
workspace_search: isWorkspaceLevel,
})
.then((res) => setIssues(res))
.finally(() => {
setIsSearching(false);
setIsLoading(false);
});
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
return (
@@ -198,7 +194,14 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
</h5>
)}
{isSearching || isLoading ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
@@ -206,59 +209,48 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
<Loader.Item height="40px" />
</Loader>
) : (
<>
{issues.length === 0 ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
) : (
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
<ul className={`text-sm text-custom-text-100 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => {
const selected = selectedIssues.some((i) => i.id === issue.id);
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex items-center gap-2">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>
{issue.name}
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
);
})}
</ul>
)}
</>
return (
<Combobox.Option
key={issue.id}
as="label"
htmlFor={`issue-${issue.id}`}
value={issue}
className={({ active }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex items-center gap-2">
<input type="checkbox" checked={selected} readOnly />
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>
{issue.name}
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
);
})}
</ul>
)}
</Combobox.Options>
</Combobox>
@@ -149,7 +149,7 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return (
<div className="relative">
<div>
<Link href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}>
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div className="flex items-center justify-between gap-2">
@@ -231,23 +231,23 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions cycleId={cycleId} projectId={projectId} workspaceSlug={workspaceSlug} />
</div>
</div>
</div>
</div>
</Link>
<div className="absolute right-4 bottom-3.5 z-[5] flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions cycleId={cycleId} projectId={projectId} workspaceSlug={workspaceSlug} />
</div>
</div>
);
});
+50 -53
View File
@@ -154,7 +154,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
return (
<div className="relative">
<>
<Link
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycleDetails.id}`}
onClick={(e) => {
@@ -199,61 +199,58 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
</div>
</div>
<span className="h-6 w-52 flex-shrink-0" />
</div>
</Link>
<div className="absolute right-5 bottom-8 z-[5] flex items-center gap-1.5">
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 md:w-auto md:flex-shrink-0 md:justify-end">
{currentCycle && (
<div
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
>
{currentCycle.value === "current"
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</div>
)}
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => {
const member = getUserDetails(assignee_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
<div className="relative flex w-full flex-shrink-0 items-center justify-between gap-2.5 md:w-auto md:flex-shrink-0 md:justify-end">
{currentCycle && (
<div
className="relative flex h-6 w-20 flex-shrink-0 items-center justify-center rounded-sm text-center text-xs"
style={{
color: currentCycle.color,
backgroundColor: `${currentCycle.color}20`,
}}
selected={!!cycleDetails.is_favorite}
/>
>
{currentCycle.value === "current"
? `${daysLeft} ${daysLeft > 1 ? "days" : "day"} left`
: `${currentCycle.label}`}
</div>
)}
<CycleQuickActions
cycleId={cycleId}
projectId={projectId}
workspaceSlug={workspaceSlug}
isArchived={isArchived}
/>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${cycleDetails.assignee_ids?.length} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center">
{cycleDetails.assignee_ids && cycleDetails.assignee_ids?.length > 0 ? (
<AvatarGroup showTooltip={false}>
{cycleDetails.assignee_ids?.map((assignee_id) => {
const member = getUserDetails(assignee_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (cycleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!cycleDetails.is_favorite}
/>
)}
<CycleQuickActions
cycleId={cycleId}
projectId={projectId}
workspaceSlug={workspaceSlug}
isArchived={isArchived}
/>
</div>
</div>
</div>
</div>
</div>
</Link>
</>
);
});
+2 -3
View File
@@ -1,5 +1,4 @@
import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
@@ -24,7 +23,7 @@ export type EmptyStateProps = {
secondaryButtonOnClick?: () => void;
};
export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
export const EmptyState: React.FC<EmptyStateProps> = (props) => {
const {
type,
size = "lg",
@@ -174,4 +173,4 @@ export const EmptyState: React.FC<EmptyStateProps> = observer((props) => {
)}
</>
);
});
};
@@ -76,8 +76,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const sub_group_by = displayFilters?.sub_group_by;
const group_by = displayFilters?.group_by;
const orderBy = displayFilters?.order_by;
const userDisplayFilters = displayFilters || null;
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
@@ -160,8 +158,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
issues.getIssueIds,
updateIssue,
group_by,
sub_group_by,
orderBy !== "sort_order"
sub_group_by
).catch((err) => {
setToast({
title: "Error",
@@ -262,7 +259,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
orderBy={orderBy}
updateIssue={updateIssue}
quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters}
@@ -60,7 +60,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
className="absolute -top-1 right-0 hidden group-hover/kanban-block:block"
onClick={handleEventPropagation}
>
{!isReadOnly && quickActions(issue)}
{quickActions(issue)}
</div>
</div>
</WithDisplayPropertiesHOC>
@@ -103,7 +103,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
issueIds,
} = props;
const cardRef = useRef<HTMLAnchorElement | null>(null);
const cardRef = useRef<HTMLDivElement | null>(null);
const {
router: { workspaceSlug },
} = useApplication();
@@ -138,7 +138,6 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
return combine(
draggable({
element,
dragHandle: element,
canDrag: () => isDragAllowed,
getInitialData: () => ({ id: issue?.id, type: "ISSUE" }),
onDragStart: () => {
@@ -152,6 +151,7 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
}),
dropTargetForElements({
element,
canDrop: (payload) => payload.source?.data?.id !== issue?.id,
getData: () => ({ id: issue?.id, type: "ISSUE" }),
onDragEnter: () => {
setIsDraggingOverBlock(true);
@@ -184,29 +184,32 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
disabled={!!issue?.tempId}
ref={cardRef}
className={cn(
"block rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
{ "hover:cursor-pointer": isDragAllowed },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id },
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
)}
>
<RenderIfVisible
classNames="space-y-2 px-3 py-2"
root={scrollableContainerRef}
defaultHeight="100px"
horizontalOffset={50}
changingReference={issueIds}
<div
className={cn(
"rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
{ "hover:cursor-pointer": isDragAllowed },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id },
{ "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
)}
ref={cardRef}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
updateIssue={updateIssue}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</RenderIfVisible>
<RenderIfVisible
classNames="space-y-2 px-3 py-2"
root={scrollableContainerRef}
defaultHeight="100px"
horizontalOffset={50}
changingReference={issueIds}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
updateIssue={updateIssue}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</RenderIfVisible>
</div>
</ControlLink>
</div>
</>
@@ -11,7 +11,6 @@ import {
TUnGroupedIssues,
TIssueKanbanFilters,
TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types";
// constants
// hooks
@@ -40,7 +39,6 @@ export interface IGroupByKanBan {
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id: string;
isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
@@ -89,7 +87,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleOnDrop,
showEmptyGroup = true,
subGroupIssueHeaderCount,
orderBy,
} = props;
const member = useMember();
@@ -183,7 +180,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
updateIssue={updateIssue}
@@ -210,7 +206,6 @@ export interface IKanBan {
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
orderBy: TIssueOrderByOptions | undefined;
sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
@@ -257,7 +252,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
handleOnDrop,
showEmptyGroup,
subGroupIssueHeaderCount,
orderBy,
} = props;
const issueKanBanView = useKanbanView();
@@ -269,7 +263,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
updateIssue={updateIssue}
@@ -11,19 +11,13 @@ import {
TSubGroupedIssues,
TUnGroupedIssues,
TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useProjectState } from "@/hooks/store";
//components
import {
KanbanDropLocation,
getSourceFromDropPayload,
getDestinationFromDropPayload,
highlightIssueOnDrop,
} from "./utils";
import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
interface IKanbanGroup {
@@ -51,7 +45,6 @@ interface IKanbanGroup {
groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
orderBy: TIssueOrderByOptions | undefined;
}
export const KanbanGroup = (props: IKanbanGroup) => {
@@ -59,7 +52,6 @@ export const KanbanGroup = (props: IKanbanGroup) => {
groupId,
sub_group_id,
group_by,
orderBy,
sub_group_by,
issuesMap,
displayProperties,
@@ -110,15 +102,13 @@ export const KanbanGroup = (props: IKanbanGroup) => {
if (!source || !destination) return;
handleOnDrop(source, destination);
highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order");
},
}),
autoScrollForElements({
element,
})
);
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]);
}, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]);
const prePopulateQuickAddData = (
groupByKey: string | undefined,
@@ -172,28 +162,16 @@ export const KanbanGroup = (props: IKanbanGroup) => {
return preloadedData;
};
const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order";
return (
<div
id={`${groupId}__${sub_group_id}`}
className={cn(
"relative h-full transition-all min-h-[50px]",
{ "bg-custom-background-80": isDraggingOverColumn },
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay }
{ "vertical-scrollbar scrollbar-md": !sub_group_by }
)}
ref={columnRef}
>
<div
className={cn(
"absolute top-0 left-0 h-full w-full justify-center items-center text-sm text-custom-text-100",
{
"flex bg-custom-primary-10 border-[2px] border-custom-primary-40 rounded z-[2]": shouldOverlay,
},
{ hidden: !shouldOverlay }
)}
>
<span>Drop here to move issue</span>
</div>
<KanbanIssueBlocksList
sub_group_id={sub_group_id}
columnId={groupId}
@@ -11,7 +11,6 @@ import {
TUnGroupedIssues,
TIssueKanbanFilters,
TIssueGroupByOptions,
TIssueOrderByOptions,
} from "@plane/types";
// components
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@@ -114,7 +113,6 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
disableIssueCreation?: boolean;
storeType: KanbanStoreType;
enableQuickIssueCreate: boolean;
orderBy: TIssueOrderByOptions | undefined;
canEditProperties: (projectId: string | undefined) => boolean;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
quickAddCallback?: (
@@ -147,7 +145,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
viewId,
scrollableContainerRef,
handleOnDrop,
orderBy,
} = props;
const calculateIssueCount = (column_id: string) => {
@@ -183,7 +180,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
return (
<div key={_list.id} className="flex flex-shrink-0 flex-col">
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky top-[50px] z-[1] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
<div className="sticky left-0 flex-shrink-0">
<HeaderSubGroupByCard
column_id={_list.id}
@@ -218,7 +215,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop}
orderBy={orderBy}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}
@@ -257,7 +253,6 @@ export interface IKanBanSwimLanes {
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
orderBy: TIssueOrderByOptions | undefined;
}
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
@@ -267,7 +262,6 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
displayProperties,
sub_group_by,
group_by,
orderBy,
updateIssue,
storeType,
quickActions,
@@ -318,7 +312,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
return (
<div className="relative">
<div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90 px-2">
<SubGroupSwimlaneHeader
issueIds={issueIds}
group_by={group_by}
@@ -339,7 +333,6 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
orderBy={orderBy}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
@@ -1,5 +1,4 @@
import pull from "lodash/pull";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
@@ -88,17 +87,14 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K
const handleSortOrder = (
destinationIssues: string[],
destinationIssueId: string | undefined,
getIssueById: (issueId: string) => TIssue | undefined,
shouldAddIssueAtTop = false
getIssueById: (issueId: string) => TIssue | undefined
) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
const destinationIndex = destinationIssueId
? destinationIssues.indexOf(destinationIssueId)
: shouldAddIssueAtTop
? 0
: destinationIssues.length;
: destinationIssues.length;
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
@@ -149,8 +145,7 @@ export const handleDragDrop = async (
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
updateIssue: ((projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined,
groupBy: TIssueGroupByOptions | undefined,
subGroupBy: TIssueGroupByOptions | undefined,
shouldAddIssueAtTop = false
subGroupBy: TIssueGroupByOptions | undefined
) => {
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
@@ -170,7 +165,7 @@ export const handleDragDrop = async (
// for both horizontal and vertical dnd
updatedIssue = {
...updatedIssue,
...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop),
...handleSortOrder(destinationIssues, destination.id, getIssueById),
};
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
@@ -212,18 +207,3 @@ export const handleDragDrop = async (
);
}
};
export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
setTimeout(async () => {
const sourceElementId = elementId ?? "";
const sourceElement = document.getElementById(sourceElementId);
if (shouldScrollIntoView && sourceElement)
await scrollIntoView(sourceElement, { behavior: "smooth", block: "center" });
sourceElement?.classList?.add("highlight");
setTimeout(() => {
const sourceElementId = elementId ?? "";
const sourceElement = document.getElementById(sourceElementId);
sourceElement?.classList?.remove("highlight");
}, 1000);
}, 200);
};
@@ -28,7 +28,7 @@ export const IssueBlocksList: FC<Props> = (props) => {
key={`${issueId}`}
defaultHeight="3rem"
root={containerRef}
classNames="relative border-b border-b-custom-border-200 last:border-b-transparent"
classNames={"relative border-b last:border-b-transparent"}
changingReference={issueIds}
>
<IssueBlock
@@ -36,7 +36,6 @@ export const ParentIssuesListModal: React.FC<Props> = ({
projectId,
issueId,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
const [isSearching, setIsSearching] = useState(false);
@@ -57,7 +56,6 @@ export const ParentIssuesListModal: React.FC<Props> = ({
if (!isOpen || !workspaceSlug || !projectId) return;
setIsSearching(true);
setIsLoading(true);
projectService
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
@@ -67,10 +65,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
workspace_search: isWorkspaceLevel,
})
.then((res) => setIssues(res))
.finally(() => {
setIsSearching(false);
setIsLoading(false);
});
.finally(() => setIsSearching(false));
}, [debouncedSearchTerm, isOpen, issueId, isWorkspaceLevel, projectId, workspaceSlug]);
return (
@@ -158,7 +153,14 @@ export const ParentIssuesListModal: React.FC<Props> = ({
</h5>
)}
{isSearching || isLoading ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />
<Loader.Item height="40px" />
@@ -166,52 +168,41 @@ export const ParentIssuesListModal: React.FC<Props> = ({
<Loader.Item height="40px" />
</Loader>
) : (
<>
{issues.length === 0 ? (
<IssueSearchModalEmptyState
debouncedSearchTerm={debouncedSearchTerm}
isSearching={isSearching}
issues={issues}
searchTerm={searchTerm}
/>
) : (
<ul className={`text-sm ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue}
className={({ active, selected }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex flex-grow items-center gap-2 truncate">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>{" "}
<span className="truncate">{issue.name}</span>
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden flex-shrink-0 text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
))}
</ul>
)}
</>
<ul className={`text-sm ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => (
<Combobox.Option
key={issue.id}
value={issue}
className={({ active, selected }) =>
`group flex w-full cursor-pointer select-none items-center justify-between gap-2 rounded-md px-3 py-2 text-custom-text-200 ${
active ? "bg-custom-background-80 text-custom-text-100" : ""
} ${selected ? "text-custom-text-100" : ""}`
}
>
<div className="flex flex-grow items-center gap-2 truncate">
<span
className="block h-1.5 w-1.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: issue.state__color,
}}
/>
<span className="flex-shrink-0 text-xs">
{issue.project__identifier}-{issue.sequence_id}
</span>{" "}
<span className="truncate">{issue.name}</span>
</div>
<a
href={`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`}
target="_blank"
className="z-1 relative hidden flex-shrink-0 text-custom-text-200 hover:text-custom-text-100 group-hover:block"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<Rocket className="h-4 w-4" />
</a>
</Combobox.Option>
))}
</ul>
)}
</Combobox.Options>
</Combobox>
+3 -3
View File
@@ -91,9 +91,9 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you wish to delete{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span>? This will remove the
label from all the issue and from any views where the label is being filtered upon.
Are you sure you want to delete label-{" "}
<span className="font-medium text-custom-text-100">{data?.name}</span>? The label will be
removed from all the issues.
</p>
</div>
</div>
+92 -93
View File
@@ -144,107 +144,106 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
: "0 Issue";
return (
<div className="relative">
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2">
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
<div className="flex items-center gap-2">
{moduleStatus && (
<span
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
style={{
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
{moduleStatus.label}
</span>
)}
<button onClick={openModuleOverview}>
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-custom-text-200">
<LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
</div>
{moduleDetails.member_ids?.length > 0 && (
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`} isMobile={isMobile}>
<div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
</div>
</Tooltip>
)}
</div>
<Tooltip
isMobile={isMobile}
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
position="top-left"
>
<div className="flex w-full items-center">
<div
className="bar relative h-1.5 w-full rounded bg-custom-background-90"
<Link href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}>
<div className="flex h-44 w-full flex-col justify-between rounded border border-custom-border-100 bg-custom-background-100 p-4 text-sm hover:shadow-md">
<div>
<div className="flex items-center justify-between gap-2">
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
<div className="flex items-center gap-2">
{moduleStatus && (
<span
className="flex h-6 w-20 items-center justify-center rounded-sm text-center text-xs"
style={{
boxShadow: "1px 1px 4px 0px rgba(161, 169, 191, 0.35) inset",
color: moduleStatus.color,
backgroundColor: `${moduleStatus.color}20`,
}}
>
<div
className="absolute left-0 top-0 h-1.5 rounded bg-blue-600 duration-300"
style={{
width: `${isNaN(completionPercentage) ? 0 : completionPercentage.toFixed(0)}%`,
}}
/>
</div>
</div>
</Tooltip>
{moduleStatus.label}
</span>
)}
<button onClick={openModuleOverview}>
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
</div>
</div>
<div className="flex items-center justify-between py-0.5">
{isDateValid ? (
<>
<span className="text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
</>
) : (
<span className="text-xs text-custom-text-400">No due date</span>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-custom-text-200">
<LayersIcon className="h-4 w-4 text-custom-text-300" />
<span className="text-xs text-custom-text-300">{issueCount ?? "0 Issue"}</span>
</div>
{moduleDetails.member_ids?.length > 0 && (
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`} isMobile={isMobile}>
<div className="flex cursor-default items-center gap-1">
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
</div>
</Tooltip>
)}
</div>
<Tooltip
isMobile={isMobile}
tooltipContent={isNaN(completionPercentage) ? "0" : `${completionPercentage.toFixed(0)}%`}
position="top-left"
>
<div className="flex w-full items-center">
<div
className="bar relative h-1.5 w-full rounded bg-custom-background-90"
style={{
boxShadow: "1px 1px 4px 0px rgba(161, 169, 191, 0.35) inset",
}}
>
<div
className="absolute left-0 top-0 h-1.5 rounded bg-blue-600 duration-300"
style={{
width: `${isNaN(completionPercentage) ? 0 : completionPercentage.toFixed(0)}%`,
}}
/>
</div>
</div>
</Tooltip>
<div className="flex items-center justify-between">
{isDateValid ? (
<>
<span className="text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
</>
) : (
<span className="text-xs text-custom-text-400">No due date</span>
)}
<div className="z-[5] flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
)}
</div>
</div>
</div>
</Link>
<div className="absolute right-4 bottom-3.5 z-[5] flex items-center gap-1.5">
{isEditingAllowed && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={!!moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
)}
</div>
</div>
</Link>
);
});
+68 -74
View File
@@ -135,7 +135,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
const completedModuleCheck = moduleDetails.status === "completed";
return (
<div className="relative">
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 sm:flex-row">
<Link
href={`/${workspaceSlug}/projects/${moduleDetails.project_id}/modules/${moduleDetails.id}`}
onClick={(e) => {
@@ -143,39 +143,33 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
openModuleOverview(e);
}
}}
className="relative flex w-full items-center justify-between gap-3 overflow-hidden"
>
<div className="group flex w-full flex-col items-center justify-between gap-5 border-b border-custom-border-100 bg-custom-background-100 px-5 py-6 text-sm hover:bg-custom-background-90 sm:flex-row">
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
</div>
<button onClick={openModuleOverview} className="z-[5] hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
<div className="relative flex w-full items-center gap-3 overflow-hidden">
<div className="flex items-center gap-4 truncate">
<span className="flex-shrink-0">
<CircularProgressIndicator size={38} percentage={progress}>
{completedModuleCheck ? (
progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-sm text-custom-primary-100">{`!`}</span>
)
) : progress === 100 ? (
<Check className="h-3 w-3 stroke-[2] text-custom-primary-100" />
) : (
<span className="text-xs text-custom-text-300">{`${progress}%`}</span>
)}
</CircularProgressIndicator>
</span>
<Tooltip tooltipContent={moduleDetails.name} position="top" isMobile={isMobile}>
<span className="truncate text-base font-medium">{moduleDetails.name}</span>
</Tooltip>
</div>
<span className="h-6 w-52 flex-shrink-0" />
<button onClick={openModuleOverview} className="z-[5] hidden flex-shrink-0 group-hover:flex">
<Info className="h-4 w-4 text-custom-text-400" />
</button>
</div>
</Link>
<div className="absolute right-5 bottom-8 z-[5] flex items-center gap-1.5">
<div className="flex flex-shrink-0 items-center justify-center">
{moduleStatus && (
<span
@@ -189,51 +183,51 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
</span>
)}
</div>
<div className="relative flex w-full items-center justify-between gap-2.5 sm:w-auto sm:flex-shrink-0 sm:justify-end ">
<div className="text-xs text-custom-text-300">
{renderDate && (
<span className=" text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
</div>
</Link>
<div className="relative flex w-full items-center justify-between gap-2.5 sm:w-auto sm:flex-shrink-0 sm:justify-end ">
<div className="text-xs text-custom-text-300">
{renderDate && (
<span className=" text-xs text-custom-text-300">
{renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
</span>
)}
</div>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center gap-1">
{moduleDetails.member_ids.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
<div className="relative flex flex-shrink-0 items-center gap-3">
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`} isMobile={isMobile}>
<div className="flex w-10 cursor-default items-center justify-center gap-1">
{moduleDetails.member_ids.length > 0 ? (
<AvatarGroup showTooltip={false}>
{moduleDetails.member_ids.map((member_id) => {
const member = getUserDetails(member_id);
return <Avatar key={member?.id} name={member?.display_name} src={member?.avatar} />;
})}
</AvatarGroup>
) : (
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
<User2 className="h-4 w-4 text-custom-text-400" />
</span>
)}
</div>
</Tooltip>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
isArchived={isArchived}
/>
)}
</div>
{isEditingAllowed && !isArchived && (
<FavoriteStar
onClick={(e) => {
if (moduleDetails.is_favorite) handleRemoveFromFavorites(e);
else handleAddToFavorites(e);
}}
selected={moduleDetails.is_favorite}
/>
)}
{workspaceSlug && projectId && (
<ModuleQuickActions
moduleId={moduleId}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
isArchived={isArchived}
/>
)}
</div>
</div>
</div>
-1
View File
@@ -58,7 +58,6 @@
"react-markdown": "^8.0.7",
"react-popper": "^2.3.0",
"sharp": "^0.32.1",
"smooth-scroll-into-view-if-needed": "^2.0.2",
"swr": "^2.1.3",
"tailwind-merge": "^2.0.0",
"use-debounce": "^9.0.4",
-7
View File
@@ -422,10 +422,8 @@ body {
}
.vertical-lr {
writing-mode: vertical-lr;
-webkit-writing-mode: vertical-lr;
-ms-writing-mode: vertical-lr;
width: fit-content;
}
div.web-view-spinner {
@@ -632,8 +630,3 @@ div.web-view-spinner div.bar12 {
.scrollbar-lg::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
}
/* highlight class */
.highlight {
border: 1px solid rgb(var(--color-primary-100));
}
+1 -20
View File
@@ -2755,7 +2755,7 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^18.2.42":
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
@@ -3699,11 +3699,6 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
compute-scroll-into-view@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -7638,13 +7633,6 @@ schema-utils@^3.1.1:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
scroll-into-view-if-needed@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f"
integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==
dependencies:
compute-scroll-into-view "^3.0.2"
selecto@~1.26.3:
version "1.26.3"
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212"
@@ -7786,13 +7774,6 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
smooth-scroll-into-view-if-needed@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-2.0.2.tgz#5bd4ebef668474d6618ce8704650082e93068371"
integrity sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q==
dependencies:
scroll-into-view-if-needed "^3.1.0"
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"