UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: новый дизайн и полировка карточек внутреннего контура
This commit is contained in:
parent
a4eaf4b247
commit
fc72c35e5a
|
|
@ -846,9 +846,13 @@ class IssueListDetailSerializer(serializers.Serializer):
|
||||||
"created_at": instance.created_at,
|
"created_at": instance.created_at,
|
||||||
"updated_at": instance.updated_at,
|
"updated_at": instance.updated_at,
|
||||||
"created_by": instance.created_by_id,
|
"created_by": instance.created_by_id,
|
||||||
|
"created_by_avatar_url": getattr(instance, "created_by_avatar_url", None),
|
||||||
|
"created_by_detail": UserLiteSerializer(instance.created_by).data if instance.created_by else None,
|
||||||
|
"created_by_display_name": getattr(instance, "created_by_display_name", None),
|
||||||
"updated_by": instance.updated_by_id,
|
"updated_by": instance.updated_by_id,
|
||||||
"is_draft": instance.is_draft,
|
"is_draft": instance.is_draft,
|
||||||
"archived_at": instance.archived_at,
|
"archived_at": instance.archived_at,
|
||||||
|
"source_project_name": getattr(instance, "source_project_name", None),
|
||||||
# Computed fields
|
# Computed fields
|
||||||
"cycle_id": instance.cycle_id,
|
"cycle_id": instance.cycle_id,
|
||||||
"module_ids": self.get_module_ids(instance),
|
"module_ids": self.get_module_ids(instance),
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,18 @@ class IssueListEndpoint(BaseAPIView):
|
||||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
created_by_display_name=F("created_by__display_name"),
|
||||||
|
created_by_avatar_url=F("created_by__avatar"),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
source_project_name=Subquery(
|
||||||
|
IntakeIssue.objects.filter(
|
||||||
|
issue_id=OuterRef("id"),
|
||||||
|
extra__bridge="external-contours",
|
||||||
|
).values("extra__source_project_name")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
.order_by()
|
.order_by()
|
||||||
|
|
@ -180,12 +192,15 @@ class IssueListEndpoint(BaseAPIView):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
"created_by_display_name",
|
||||||
|
"created_by_avatar_url",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"attachment_count",
|
"attachment_count",
|
||||||
"link_count",
|
"link_count",
|
||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
"deleted_at",
|
"deleted_at",
|
||||||
|
"source_project_name",
|
||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
|
issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
|
||||||
|
|
@ -206,7 +221,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
issues = Issue.issue_objects.filter(
|
issues = Issue.issue_objects.filter(
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
).distinct()
|
).select_related("created_by").distinct()
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
@ -217,6 +232,18 @@ class IssueViewSet(BaseViewSet):
|
||||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.annotate(
|
||||||
|
created_by_display_name=F("created_by__display_name"),
|
||||||
|
created_by_avatar_url=F("created_by__avatar"),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
source_project_name=Subquery(
|
||||||
|
IntakeIssue.objects.filter(
|
||||||
|
issue_id=OuterRef("id"),
|
||||||
|
extra__bridge="external-contours",
|
||||||
|
).values("extra__source_project_name")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
link_count=Subquery(
|
link_count=Subquery(
|
||||||
IssueLink.objects.filter(issue=OuterRef("id"))
|
IssueLink.objects.filter(issue=OuterRef("id"))
|
||||||
|
|
@ -445,6 +472,8 @@ class IssueViewSet(BaseViewSet):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
"created_by_display_name",
|
||||||
|
"created_by_avatar_url",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"attachment_count",
|
"attachment_count",
|
||||||
"link_count",
|
"link_count",
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,15 @@ def issue_on_results(
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
"created_by_display_name",
|
||||||
|
"created_by_avatar_url",
|
||||||
"updated_by",
|
"updated_by",
|
||||||
"attachment_count",
|
"attachment_count",
|
||||||
"link_count",
|
"link_count",
|
||||||
"is_draft",
|
"is_draft",
|
||||||
"archived_at",
|
"archived_at",
|
||||||
"state__group",
|
"state__group",
|
||||||
|
"source_project_name",
|
||||||
]
|
]
|
||||||
|
|
||||||
if group_by in FIELD_MAPPER:
|
if group_by in FIELD_MAPPER:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { BUTTON_VARIANTS_WITH_TEXT } from "./constants";
|
||||||
import type { TDropdownProps } from "./types";
|
import type { TDropdownProps } from "./types";
|
||||||
|
|
||||||
type Props = TDropdownProps & {
|
type Props = TDropdownProps & {
|
||||||
|
button?: React.ReactNode;
|
||||||
clearIconClassName?: string;
|
clearIconClassName?: string;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
optionsClassName?: string;
|
optionsClassName?: string;
|
||||||
|
|
@ -47,6 +48,7 @@ type Props = TDropdownProps & {
|
||||||
export const DateDropdown = observer(function DateDropdown(props: Props) {
|
export const DateDropdown = observer(function DateDropdown(props: Props) {
|
||||||
const {
|
const {
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
|
button,
|
||||||
buttonContainerClassName,
|
buttonContainerClassName,
|
||||||
buttonVariant,
|
buttonVariant,
|
||||||
className = "",
|
className = "",
|
||||||
|
|
@ -121,47 +123,61 @@ export const DateDropdown = observer(function DateDropdown(props: Props) {
|
||||||
if (maxDate) disabledDays.push({ after: maxDate });
|
if (maxDate) disabledDays.push({ after: maxDate });
|
||||||
|
|
||||||
const comboButton = (
|
const comboButton = (
|
||||||
<button
|
<>
|
||||||
type="button"
|
{button ? (
|
||||||
className={cn(
|
<button
|
||||||
"clickable block h-full max-w-full outline-none",
|
type="button"
|
||||||
{
|
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
|
||||||
"cursor-not-allowed text-secondary": disabled,
|
ref={setReferenceElement}
|
||||||
"cursor-pointer": !disabled,
|
onClick={handleOnClick}
|
||||||
},
|
disabled={disabled}
|
||||||
buttonContainerClassName
|
>
|
||||||
|
{button}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"clickable block h-full max-w-full outline-none",
|
||||||
|
{
|
||||||
|
"cursor-not-allowed text-secondary": disabled,
|
||||||
|
"cursor-pointer": !disabled,
|
||||||
|
},
|
||||||
|
buttonContainerClassName
|
||||||
|
)}
|
||||||
|
ref={setReferenceElement}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<DropdownButton
|
||||||
|
className={buttonClassName}
|
||||||
|
isActive={isOpen}
|
||||||
|
tooltipHeading={placeholder}
|
||||||
|
tooltipContent={value ? renderFormattedDate(value, formatToken) : "None"}
|
||||||
|
showTooltip={showTooltip}
|
||||||
|
variant={buttonVariant}
|
||||||
|
renderToolTipByDefault={renderByDefault}
|
||||||
|
>
|
||||||
|
{!hideIcon && icon}
|
||||||
|
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||||
|
<span className={cn("flex-grow truncate text-left text-body-xs-medium", labelClassName)}>
|
||||||
|
{value ? renderFormattedDate(value, formatToken) : placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isClearable && !disabled && isDateSelected && (
|
||||||
|
<CloseIcon
|
||||||
|
className={cn("h-2.5 w-2.5 flex-shrink-0", clearIconClassName)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownButton>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
ref={setReferenceElement}
|
</>
|
||||||
onClick={handleOnClick}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<DropdownButton
|
|
||||||
className={buttonClassName}
|
|
||||||
isActive={isOpen}
|
|
||||||
tooltipHeading={placeholder}
|
|
||||||
tooltipContent={value ? renderFormattedDate(value, formatToken) : "None"}
|
|
||||||
showTooltip={showTooltip}
|
|
||||||
variant={buttonVariant}
|
|
||||||
renderToolTipByDefault={renderByDefault}
|
|
||||||
>
|
|
||||||
{!hideIcon && icon}
|
|
||||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
|
||||||
<span className={cn("flex-grow truncate text-left text-body-xs-medium", labelClassName)}>
|
|
||||||
{value ? renderFormattedDate(value, formatToken) : placeholder}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isClearable && !disabled && isDateSelected && (
|
|
||||||
<CloseIcon
|
|
||||||
className={cn("h-2.5 w-2.5 flex-shrink-0", clearIconClassName)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
onChange(null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DropdownButton>
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import type { IQuickActionProps, TRenderQuickActions } from "../list/list-view-t
|
||||||
import { getSourceFromDropPayload } from "../utils";
|
import { getSourceFromDropPayload } from "../utils";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
export type KanbanStoreType =
|
export type KanbanStoreType =
|
||||||
| EIssuesStoreType.PROJECT
|
| EIssuesStoreType.PROJECT
|
||||||
|
|
@ -47,6 +48,7 @@ export interface IBaseKanBanLayout {
|
||||||
QuickActions: FC<IQuickActionProps>;
|
QuickActions: FC<IQuickActionProps>;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
isCompletedCycle?: boolean;
|
isCompletedCycle?: boolean;
|
||||||
viewId?: string | undefined;
|
viewId?: string | undefined;
|
||||||
isEpic?: boolean;
|
isEpic?: boolean;
|
||||||
|
|
@ -57,6 +59,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
|
||||||
QuickActions,
|
QuickActions,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditPropertiesBasedOnProject,
|
canEditPropertiesBasedOnProject,
|
||||||
|
cardVariant = "default",
|
||||||
isCompletedCycle = false,
|
isCompletedCycle = false,
|
||||||
viewId,
|
viewId,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
|
|
@ -284,6 +287,7 @@ export const BaseKanBanRoot = observer(function BaseKanBanRoot(props: IBaseKanBa
|
||||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed || isCompletedCycle}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
cardVariant={cardVariant}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleOnDrop}
|
||||||
loadMoreIssues={fetchMoreIssues}
|
loadMoreIssues={fetchMoreIssues}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import { IssueStats } from "@/plane-web/components/issues/issue-layouts/issue-st
|
||||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { IssueProperties } from "../properties/all-properties";
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import { InternalContourKanbanCard } from "./internal-contour-card";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
|
@ -54,6 +56,7 @@ interface IssueBlockProps {
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
shouldRenderByDefault?: boolean;
|
shouldRenderByDefault?: boolean;
|
||||||
isEpic?: boolean;
|
isEpic?: boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IssueDetailsBlockProps {
|
interface IssueDetailsBlockProps {
|
||||||
|
|
@ -64,10 +67,22 @@ interface IssueDetailsBlockProps {
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
isEpic?: boolean;
|
isEpic?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props: IssueDetailsBlockProps) {
|
const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props: IssueDetailsBlockProps) {
|
||||||
const { cardRef, issue, updateIssue, quickActions, isReadOnly, displayProperties, isEpic = false } = props;
|
const {
|
||||||
|
cardRef,
|
||||||
|
issue,
|
||||||
|
updateIssue,
|
||||||
|
quickActions,
|
||||||
|
isReadOnly,
|
||||||
|
displayProperties,
|
||||||
|
isEpic = false,
|
||||||
|
isActive = false,
|
||||||
|
cardVariant = "default",
|
||||||
|
} = props;
|
||||||
// refs
|
// refs
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
// states
|
// states
|
||||||
|
|
@ -97,6 +112,20 @@ const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props:
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
if (cardVariant === "internal-contour") {
|
||||||
|
return (
|
||||||
|
<InternalContourKanbanCard
|
||||||
|
cardRef={cardRef}
|
||||||
|
issue={issue}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
updateIssue={updateIssue}
|
||||||
|
quickActions={quickActions}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -168,6 +197,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
shouldRenderByDefault,
|
shouldRenderByDefault,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
|
cardVariant = "default",
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const cardRef = useRef<HTMLAnchorElement | null>(null);
|
const cardRef = useRef<HTMLAnchorElement | null>(null);
|
||||||
|
|
@ -194,6 +224,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
|
|
||||||
const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties;
|
const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties;
|
||||||
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
||||||
|
const isPeeked = issue ? getIsIssuePeeked(issue.id) : false;
|
||||||
|
|
||||||
const workItemLink = generateWorkItemLink({
|
const workItemLink = generateWorkItemLink({
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
|
|
@ -275,10 +306,18 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
href={workItemLink}
|
href={workItemLink}
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"block w-full rounded-lg border border-subtle bg-layer-2 p-3 text-13 shadow-raised-100 outline-[0.5px] outline-transparent transition-all hover:border-strong hover:shadow-raised-200",
|
"block w-full text-13 transition-all",
|
||||||
|
cardVariant === "internal-contour"
|
||||||
|
? "rounded-[28px] border-0 p-4 shadow-none outline-none ring-0 hover:border-0 hover:outline-none hover:ring-0"
|
||||||
|
: "rounded-lg border border-subtle bg-layer-2 p-3 shadow-raised-100 outline-[0.5px] outline-transparent hover:border-strong hover:shadow-raised-200",
|
||||||
{ "hover:cursor-pointer": isDragAllowed },
|
{ "hover:cursor-pointer": isDragAllowed },
|
||||||
{ "border border-accent-strong hover:border-accent-strong": getIsIssuePeeked(issue.id) },
|
{
|
||||||
{ "z-[100] bg-layer-1": isCurrentBlockDragging }
|
"bg-[#C3FF66] text-[#111111]": cardVariant === "internal-contour" && isPeeked,
|
||||||
|
"bg-[#2A2B2E] text-white": cardVariant === "internal-contour" && !isPeeked,
|
||||||
|
"border border-accent-strong hover:border-accent-strong": cardVariant !== "internal-contour" && isPeeked,
|
||||||
|
},
|
||||||
|
{ "z-[100] bg-layer-1": isCurrentBlockDragging && cardVariant !== "internal-contour" },
|
||||||
|
{ "z-[100] bg-[#C3FF66]": isCurrentBlockDragging && cardVariant === "internal-contour" }
|
||||||
)}
|
)}
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
disabled={!!issue?.tempId}
|
disabled={!!issue?.tempId}
|
||||||
|
|
@ -286,7 +325,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
<RenderIfVisible
|
<RenderIfVisible
|
||||||
classNames="space-y-2"
|
classNames="space-y-2"
|
||||||
root={scrollableContainerRef}
|
root={scrollableContainerRef}
|
||||||
defaultHeight="100px"
|
defaultHeight={cardVariant === "internal-contour" ? "220px" : "100px"}
|
||||||
horizontalOffset={100}
|
horizontalOffset={100}
|
||||||
verticalOffset={200}
|
verticalOffset={200}
|
||||||
defaultValue={shouldRenderByDefault}
|
defaultValue={shouldRenderByDefault}
|
||||||
|
|
@ -299,6 +338,8 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
isReadOnly={!canEditIssueProperties}
|
isReadOnly={!canEditIssueProperties}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
|
isActive={isPeeked}
|
||||||
|
cardVariant={cardVariant}
|
||||||
/>
|
/>
|
||||||
</RenderIfVisible>
|
</RenderIfVisible>
|
||||||
</ControlLink>
|
</ControlLink>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import type { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
// local imports
|
// local imports
|
||||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { KanbanIssueBlock } from "./block";
|
import { KanbanIssueBlock } from "./block";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
interface IssueBlocksListProps {
|
interface IssueBlocksListProps {
|
||||||
sub_group_id: string;
|
sub_group_id: string;
|
||||||
|
|
@ -21,6 +22,7 @@ interface IssueBlocksListProps {
|
||||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
quickActions: TRenderQuickActions;
|
quickActions: TRenderQuickActions;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
canDropOverIssue: boolean;
|
canDropOverIssue: boolean;
|
||||||
canDragIssuesInCurrentGrouping: boolean;
|
canDragIssuesInCurrentGrouping: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
|
@ -39,6 +41,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
|
||||||
updateIssue,
|
updateIssue,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
cardVariant = "default",
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
isEpic = false,
|
isEpic = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -66,6 +69,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
draggableId={draggableId}
|
draggableId={draggableId}
|
||||||
|
cardVariant={cardVariant}
|
||||||
canDropOverIssue={canDropOverIssue}
|
canDropOverIssue={canDropOverIssue}
|
||||||
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import { getGroupByColumns, isWorkspaceLevel, getApproximateCardHeight } from ".
|
||||||
// components
|
// components
|
||||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||||
import { KanbanGroup } from "./kanban-group";
|
import { KanbanGroup } from "./kanban-group";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
export interface IKanBan {
|
export interface IKanBan {
|
||||||
issuesMap: IIssueMap;
|
issuesMap: IIssueMap;
|
||||||
|
|
@ -62,6 +63,7 @@ export interface IKanBan {
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||||
showEmptyGroup?: boolean;
|
showEmptyGroup?: boolean;
|
||||||
|
|
@ -88,6 +90,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
cardVariant = "default",
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
handleOnDrop,
|
handleOnDrop,
|
||||||
showEmptyGroup = true,
|
showEmptyGroup = true,
|
||||||
|
|
@ -224,6 +227,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
cardVariant={cardVariant}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
loadMoreIssues={loadMoreIssues}
|
loadMoreIssues={loadMoreIssues}
|
||||||
handleOnDrop={handleOnDrop}
|
handleOnDrop={handleOnDrop}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
|
import { CalendarDays, MoreHorizontal } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { useOutsideClickDetector } from "@plane/hooks";
|
||||||
|
import { PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
|
||||||
|
import type { IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
|
import { Avatar } from "@plane/ui";
|
||||||
|
import { cn, getFileURL, renderFormattedDate, renderFormattedPayloadDate } from "@plane/utils";
|
||||||
|
import { DateDropdown } from "@/components/dropdowns/date";
|
||||||
|
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||||
|
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
|
||||||
|
import { PriorityDropdown } from "@/components/dropdowns/priority";
|
||||||
|
import { StateDropdown } from "@/components/dropdowns/state/dropdown";
|
||||||
|
import { useMember } from "@/hooks/store/use-member";
|
||||||
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
|
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||||
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
cardRef: React.RefObject<HTMLElement>;
|
||||||
|
issue: TIssue;
|
||||||
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
|
quickActions: TRenderQuickActions;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const basePillClasses =
|
||||||
|
"inline-flex items-center gap-1.5 rounded-full border-0 px-2.5 py-1 text-[11px] font-medium shadow-none outline-none transition-colors";
|
||||||
|
|
||||||
|
export const InternalContourKanbanCard = observer(function InternalContourKanbanCard(props: Props) {
|
||||||
|
const { cardRef, issue, updateIssue, quickActions, isReadOnly, isActive } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isMobile } = usePlatformOS();
|
||||||
|
const { getUserDetails } = useMember();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const { getStateById, getProjectStateIds } = useProjectState();
|
||||||
|
|
||||||
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
|
||||||
|
const creatorDetails = useMemo(() => {
|
||||||
|
if (issue.created_by_detail) return issue.created_by_detail;
|
||||||
|
if (issue.created_by && getUserDetails(issue.created_by)) return getUserDetails(issue.created_by);
|
||||||
|
if (issue.created_by_display_name || issue.created_by_avatar_url) {
|
||||||
|
return {
|
||||||
|
id: issue.created_by,
|
||||||
|
display_name: issue.created_by_display_name ?? t("common.none"),
|
||||||
|
avatar_url: issue.created_by_avatar_url ?? "",
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
is_bot: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [
|
||||||
|
getUserDetails,
|
||||||
|
issue.created_by,
|
||||||
|
issue.created_by_avatar_url,
|
||||||
|
issue.created_by_detail,
|
||||||
|
issue.created_by_display_name,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sourceContourName = issue.source_project_name ?? getProjectById(issue.project_id)?.name ?? t("common.none");
|
||||||
|
const selectedState = getStateById(issue.state_id);
|
||||||
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
|
const foregroundClasses = isActive ? "text-[#111111]" : "text-white";
|
||||||
|
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
||||||
|
const pillBackgroundClasses = isActive ? "bg-black/10 text-[#111111]" : "bg-[#1B1B1F] text-white";
|
||||||
|
const iconBubbleClasses = isActive ? "bg-black text-[#C3FF66]" : "bg-[#111214] text-white";
|
||||||
|
const statusIconColor = selectedState?.color ?? (isActive ? "#111111" : "var(--text-color-primary)");
|
||||||
|
|
||||||
|
const handleEventPropagation = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
|
const creatorName = creatorDetails?.display_name ?? t("common.none");
|
||||||
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
|
||||||
|
const customActionButton = (
|
||||||
|
<div
|
||||||
|
ref={menuActionRef}
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 cursor-pointer items-center justify-center rounded-full p-1 transition-colors",
|
||||||
|
isActive ? "bg-black text-[#C3FF66] hover:bg-black/90" : "bg-[#111214] text-white hover:bg-[#0A0B0C]",
|
||||||
|
isMenuActive && (isActive ? "bg-black/90" : "bg-[#0A0B0C]")
|
||||||
|
)}
|
||||||
|
onClick={() => setIsMenuActive(!isMenuActive)}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative flex min-h-[220px] flex-col gap-4", foregroundClasses)}>
|
||||||
|
<div
|
||||||
|
className="absolute right-0 top-0 flex items-center gap-2"
|
||||||
|
onClick={handleEventPropagation}
|
||||||
|
>
|
||||||
|
{quickActions({
|
||||||
|
issue,
|
||||||
|
parentRef: cardRef,
|
||||||
|
customActionButton,
|
||||||
|
})}
|
||||||
|
<PriorityDropdown
|
||||||
|
value={issue.priority}
|
||||||
|
onChange={(priority) => updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 items-center justify-center rounded-full border-0 shadow-none outline-none",
|
||||||
|
iconBubbleClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PriorityIcon priority={issue.priority} className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StateDropdown
|
||||||
|
projectId={issue.project_id ?? undefined}
|
||||||
|
stateIds={projectStateIds ?? []}
|
||||||
|
value={issue.state_id}
|
||||||
|
onChange={(stateId) => updateIssue?.(issue.project_id ?? null, issue.id, { state_id: stateId })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full", iconBubbleClasses)}>
|
||||||
|
<StateGroupIcon
|
||||||
|
stateGroup={selectedState?.group ?? "backlog"}
|
||||||
|
color={statusIconColor}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
percentage={selectedState?.order}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pr-28">
|
||||||
|
<Avatar
|
||||||
|
src={getFileURL(creatorDetails?.avatar_url ?? "")}
|
||||||
|
name={creatorName}
|
||||||
|
size="md"
|
||||||
|
showTooltip={!isMobile}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className={cn("truncate text-body-sm-medium", foregroundClasses)}>{creatorName}</div>
|
||||||
|
<div className={cn("truncate text-[11px] font-medium", subtleTextClasses)}>{sourceContourName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 items-center justify-center px-5 text-center">
|
||||||
|
<div className="line-clamp-4 max-w-full text-lg font-semibold leading-6">{issue.name}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end justify-between gap-3">
|
||||||
|
<MemberDropdown
|
||||||
|
projectId={issue.project_id ?? undefined}
|
||||||
|
value={issue.assignee_ids}
|
||||||
|
onChange={(assigneeIds) => updateIssue?.(issue.project_id ?? null, issue.id, { assignee_ids: assigneeIds })}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div className={cn(basePillClasses, pillBackgroundClasses, "min-h-9 pl-1 pr-2")}>
|
||||||
|
<ButtonAvatars showTooltip={false} userIds={issue.assignee_ids} size="sm" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<DateDropdown
|
||||||
|
value={issue.target_date}
|
||||||
|
onChange={(targetDate) =>
|
||||||
|
updateIssue?.(issue.project_id ?? null, issue.id, {
|
||||||
|
target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={isReadOnly || !updateIssue}
|
||||||
|
button={
|
||||||
|
<div className={cn(basePillClasses, pillBackgroundClasses)}>
|
||||||
|
<CalendarDays className="h-3.5 w-3.5" />
|
||||||
|
<span className="truncate">{dueDateLabel}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -46,6 +46,7 @@ import { GroupDragOverlay } from "../group-drag-overlay";
|
||||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
|
import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
|
||||||
import { KanbanIssueBlocksList } from "./blocks-list";
|
import { KanbanIssueBlocksList } from "./blocks-list";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
interface IKanbanGroup {
|
interface IKanbanGroup {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
|
@ -65,6 +66,7 @@ interface IKanbanGroup {
|
||||||
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
|
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
groupByVisibilityToggle?: boolean;
|
groupByVisibilityToggle?: boolean;
|
||||||
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise<void>;
|
||||||
|
|
@ -87,6 +89,7 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
|
||||||
updateIssue,
|
updateIssue,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
cardVariant = "default",
|
||||||
loadMoreIssues,
|
loadMoreIssues,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
|
|
@ -305,6 +308,7 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
|
||||||
updateIssue={updateIssue}
|
updateIssue={updateIssue}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
cardVariant={cardVariant}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
canDropOverIssue={!canOverlayBeVisible}
|
canDropOverIssue={!canOverlayBeVisible}
|
||||||
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const KanBanLayout = observer(function KanBanLayout() {
|
||||||
<BaseKanBanRoot
|
<BaseKanBanRoot
|
||||||
QuickActions={ProjectIssueQuickActions}
|
QuickActions={ProjectIssueQuickActions}
|
||||||
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
|
canEditPropertiesBasedOnProject={canEditPropertiesBasedOnProject}
|
||||||
|
cardVariant="internal-contour"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { getGroupByColumns, isWorkspaceLevel } from "../utils";
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||||
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
|
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
|
||||||
|
import type { TKanbanCardVariant } from "./types";
|
||||||
|
|
||||||
interface ISubGroupSwimlaneHeader {
|
interface ISubGroupSwimlaneHeader {
|
||||||
collapsedGroups: TIssueKanbanFilters;
|
collapsedGroups: TIssueKanbanFilters;
|
||||||
|
|
@ -107,6 +108,7 @@ const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({
|
||||||
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
collapsedGroups: TIssueKanbanFilters;
|
collapsedGroups: TIssueKanbanFilters;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
|
@ -134,6 +136,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
|
||||||
const {
|
const {
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
cardVariant = "default",
|
||||||
collapsedGroups,
|
collapsedGroups,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
displayProperties,
|
displayProperties,
|
||||||
|
|
@ -216,6 +219,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
cardVariant={cardVariant}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
|
@ -238,6 +242,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
|
||||||
export interface IKanBanSwimLanes {
|
export interface IKanBanSwimLanes {
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
cardVariant?: TKanbanCardVariant;
|
||||||
collapsedGroups: TIssueKanbanFilters;
|
collapsedGroups: TIssueKanbanFilters;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
|
@ -282,6 +287,7 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
cardVariant = "default",
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
scrollableContainerRef,
|
scrollableContainerRef,
|
||||||
|
|
@ -341,6 +347,7 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
cardVariant={cardVariant}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
scrollableContainerRef={scrollableContainerRef}
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
isEpic={isEpic}
|
isEpic={isEpic}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type TKanbanCardVariant = "default" | "internal-contour";
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import type { TIssuePriorities } from "../issues";
|
import type { TIssuePriorities } from "../issues";
|
||||||
import type { TStateGroups } from "../state";
|
import type { TStateGroups } from "../state";
|
||||||
|
import type { IUserLite } from "../users";
|
||||||
import type { TIssuePublicComment } from "./activity/issue_comment";
|
import type { TIssuePublicComment } from "./activity/issue_comment";
|
||||||
import type { TIssueAttachment } from "./issue_attachment";
|
import type { TIssueAttachment } from "./issue_attachment";
|
||||||
import type { TIssueLink } from "./issue_link";
|
import type { TIssueLink } from "./issue_link";
|
||||||
|
|
@ -90,12 +91,16 @@ type IssueRelation = {
|
||||||
export type TIssue = TBaseIssue & {
|
export type TIssue = TBaseIssue & {
|
||||||
description_html?: string;
|
description_html?: string;
|
||||||
is_subscribed?: boolean;
|
is_subscribed?: boolean;
|
||||||
|
created_by_avatar_url?: string | null;
|
||||||
|
created_by_detail?: Pick<IUserLite, "id" | "display_name" | "avatar_url"> | null;
|
||||||
|
created_by_display_name?: string | null;
|
||||||
parent?: Partial<TBaseIssue>;
|
parent?: Partial<TBaseIssue>;
|
||||||
issue_reactions?: TIssueReaction[];
|
issue_reactions?: TIssueReaction[];
|
||||||
issue_attachments?: TIssueAttachment[];
|
issue_attachments?: TIssueAttachment[];
|
||||||
issue_link?: TIssueLink[];
|
issue_link?: TIssueLink[];
|
||||||
issue_relation?: IssueRelation[];
|
issue_relation?: IssueRelation[];
|
||||||
issue_related?: IssueRelation[];
|
issue_related?: IssueRelation[];
|
||||||
|
source_project_name?: string | null;
|
||||||
// tempId is used for optimistic updates. It is not a part of the API response.
|
// tempId is used for optimistic updates. It is not a part of the API response.
|
||||||
tempId?: string;
|
tempId?: string;
|
||||||
// sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response.
|
// sourceIssueId is used to store the original issue id when creating a copy of an issue. Used in cloning property values. It is not a part of the API response.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue