@@ -168,6 +197,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
scrollableContainerRef,
shouldRenderByDefault,
isEpic = false,
+ cardVariant = "default",
} = props;
const cardRef = useRef
(null);
@@ -194,6 +224,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
const isDragAllowed = canDragIssuesInCurrentGrouping && !issue?.tempId && canEditIssueProperties;
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
+ const isPeeked = issue ? getIsIssuePeeked(issue.id) : false;
const workItemLink = generateWorkItemLink({
workspaceSlug,
@@ -275,10 +306,18 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
href={workItemLink}
ref={cardRef}
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 },
- { "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)}
disabled={!!issue?.tempId}
@@ -286,7 +325,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx
index 9361e4c..616cf9e 100644
--- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/blocks-list.tsx
@@ -11,6 +11,7 @@ import type { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
// local imports
import type { TRenderQuickActions } from "../list/list-view-types";
import { KanbanIssueBlock } from "./block";
+import type { TKanbanCardVariant } from "./types";
interface IssueBlocksListProps {
sub_group_id: string;
@@ -21,6 +22,7 @@ interface IssueBlocksListProps {
updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined;
quickActions: TRenderQuickActions;
canEditProperties: (projectId: string | undefined) => boolean;
+ cardVariant?: TKanbanCardVariant;
canDropOverIssue: boolean;
canDragIssuesInCurrentGrouping: boolean;
scrollableContainerRef?: MutableRefObject;
@@ -39,6 +41,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
updateIssue,
quickActions,
canEditProperties,
+ cardVariant = "default",
scrollableContainerRef,
isEpic = false,
} = props;
@@ -66,6 +69,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
updateIssue={updateIssue}
quickActions={quickActions}
draggableId={draggableId}
+ cardVariant={cardVariant}
canDropOverIssue={canDropOverIssue}
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
canEditProperties={canEditProperties}
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx
index b7326ee..0439fce 100644
--- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/default.tsx
@@ -35,6 +35,7 @@ import { getGroupByColumns, isWorkspaceLevel, getApproximateCardHeight } from ".
// components
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
+import type { TKanbanCardVariant } from "./types";
export interface IKanBan {
issuesMap: IIssueMap;
@@ -62,6 +63,7 @@ export interface IKanBan {
disableIssueCreation?: boolean;
addIssuesToView?: (issueIds: string[]) => Promise;
canEditProperties: (projectId: string | undefined) => boolean;
+ cardVariant?: TKanbanCardVariant;
scrollableContainerRef?: MutableRefObject;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise;
showEmptyGroup?: boolean;
@@ -88,6 +90,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
disableIssueCreation,
addIssuesToView,
canEditProperties,
+ cardVariant = "default",
scrollableContainerRef,
handleOnDrop,
showEmptyGroup = true,
@@ -224,6 +227,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
quickAddCallback={quickAddCallback}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
+ cardVariant={cardVariant}
scrollableContainerRef={scrollableContainerRef}
loadMoreIssues={loadMoreIssues}
handleOnDrop={handleOnDrop}
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx
new file mode 100644
index 0000000..cc8bce9
--- /dev/null
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx
@@ -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;
+ issue: TIssue;
+ displayProperties: IIssueDisplayProperties | undefined;
+ updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | 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(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 = (
+ setIsMenuActive(!isMenuActive)}
+ >
+
+
+ );
+
+ return (
+
+
+ {quickActions({
+ issue,
+ parentRef: cardRef,
+ customActionButton,
+ })}
+
updateIssue?.(issue.project_id ?? null, issue.id, { priority })}
+ disabled={isReadOnly || !updateIssue}
+ button={
+
+ }
+ />
+ updateIssue?.(issue.project_id ?? null, issue.id, { state_id: stateId })}
+ disabled={isReadOnly || !updateIssue}
+ button={
+
+
+
+ }
+ />
+
+
+
+
+
+
{creatorName}
+
{sourceContourName}
+
+
+
+
+
+
+
updateIssue?.(issue.project_id ?? null, issue.id, { assignee_ids: assigneeIds })}
+ disabled={isReadOnly || !updateIssue}
+ button={
+
+
+
+ }
+ />
+
+
+
+ updateIssue?.(issue.project_id ?? null, issue.id, {
+ target_date: targetDate ? renderFormattedPayloadDate(targetDate) : null,
+ })
+ }
+ disabled={isReadOnly || !updateIssue}
+ button={
+
+
+ {dueDateLabel}
+
+ }
+ />
+
+
+
+ );
+});
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx
index 1358110..537e404 100644
--- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/kanban-group.tsx
@@ -46,6 +46,7 @@ import { GroupDragOverlay } from "../group-drag-overlay";
import type { TRenderQuickActions } from "../list/list-view-types";
import { KanbanQuickAddIssueButton, QuickAddIssueRoot } from "../quick-add";
import { KanbanIssueBlocksList } from "./blocks-list";
+import type { TKanbanCardVariant } from "./types";
interface IKanbanGroup {
groupId: string;
@@ -65,6 +66,7 @@ interface IKanbanGroup {
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
+ cardVariant?: TKanbanCardVariant;
groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject;
handleOnDrop: (source: GroupDropLocation, destination: GroupDropLocation) => Promise;
@@ -87,6 +89,7 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
updateIssue,
quickActions,
canEditProperties,
+ cardVariant = "default",
loadMoreIssues,
enableQuickIssueCreate,
disableIssueCreation,
@@ -305,6 +308,7 @@ export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
updateIssue={updateIssue}
quickActions={quickActions}
canEditProperties={canEditProperties}
+ cardVariant={cardVariant}
scrollableContainerRef={scrollableContainerRef}
canDropOverIssue={!canOverlayBeVisible}
canDragIssuesInCurrentGrouping={canDragIssuesInCurrentGrouping}
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/roots/project-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/roots/project-root.tsx
index 6d691ad..88c187f 100644
--- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/roots/project-root.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/roots/project-root.tsx
@@ -32,6 +32,7 @@ export const KanBanLayout = observer(function KanBanLayout() {
);
});
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx
index 84998f3..fadbea5 100644
--- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/swimlanes.tsx
@@ -31,6 +31,7 @@ import { getGroupByColumns, isWorkspaceLevel } from "../utils";
import { KanBan } from "./default";
import { HeaderGroupByCard } from "./headers/group-by-card";
import { HeaderSubGroupByCard } from "./headers/sub-group-by-card";
+import type { TKanbanCardVariant } from "./types";
interface ISubGroupSwimlaneHeader {
collapsedGroups: TIssueKanbanFilters;
@@ -107,6 +108,7 @@ const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
addIssuesToView?: (issueIds: string[]) => Promise;
canEditProperties: (projectId: string | undefined) => boolean;
+ cardVariant?: TKanbanCardVariant;
collapsedGroups: TIssueKanbanFilters;
disableIssueCreation?: boolean;
displayProperties: IIssueDisplayProperties | undefined;
@@ -134,6 +136,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
const {
addIssuesToView,
canEditProperties,
+ cardVariant = "default",
collapsedGroups,
disableIssueCreation,
displayProperties,
@@ -216,6 +219,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
enableQuickIssueCreate={enableQuickIssueCreate}
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
+ cardVariant={cardVariant}
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
scrollableContainerRef={scrollableContainerRef}
@@ -238,6 +242,7 @@ const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwim
export interface IKanBanSwimLanes {
addIssuesToView?: (issueIds: string[]) => Promise;
canEditProperties: (projectId: string | undefined) => boolean;
+ cardVariant?: TKanbanCardVariant;
collapsedGroups: TIssueKanbanFilters;
disableIssueCreation?: boolean;
displayProperties: IIssueDisplayProperties | undefined;
@@ -282,6 +287,7 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
disableIssueCreation,
enableQuickIssueCreate,
canEditProperties,
+ cardVariant = "default",
addIssuesToView,
quickAddCallback,
scrollableContainerRef,
@@ -341,6 +347,7 @@ export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanS
enableQuickIssueCreate={enableQuickIssueCreate}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
+ cardVariant={cardVariant}
quickAddCallback={quickAddCallback}
scrollableContainerRef={scrollableContainerRef}
isEpic={isEpic}
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/types.ts b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/types.ts
new file mode 100644
index 0000000..ac8766c
--- /dev/null
+++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/types.ts
@@ -0,0 +1 @@
+export type TKanbanCardVariant = "default" | "internal-contour";
diff --git a/plane-src/packages/types/src/issues/issue.ts b/plane-src/packages/types/src/issues/issue.ts
index 8054b4c..7626561 100644
--- a/plane-src/packages/types/src/issues/issue.ts
+++ b/plane-src/packages/types/src/issues/issue.ts
@@ -6,6 +6,7 @@
import type { TIssuePriorities } from "../issues";
import type { TStateGroups } from "../state";
+import type { IUserLite } from "../users";
import type { TIssuePublicComment } from "./activity/issue_comment";
import type { TIssueAttachment } from "./issue_attachment";
import type { TIssueLink } from "./issue_link";
@@ -90,12 +91,16 @@ type IssueRelation = {
export type TIssue = TBaseIssue & {
description_html?: string;
is_subscribed?: boolean;
+ created_by_avatar_url?: string | null;
+ created_by_detail?: Pick | null;
+ created_by_display_name?: string | null;
parent?: Partial;
issue_reactions?: TIssueReaction[];
issue_attachments?: TIssueAttachment[];
issue_link?: TIssueLink[];
issue_relation?: 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?: 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.