This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,185 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { Paperclip } from "lucide-react";
import { LinkIcon, ViewsIcon } from "@plane/propel/icons";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
import type { IIssueDisplayProperties } from "@plane/types";
import { cn } from "@plane/utils";
// components
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
// helpers
import { getDate } from "@/helpers/date-time.helper";
//// hooks
import type { IIssue } from "@/types/issue";
import { IssueBlockCycle } from "./cycle";
import { IssueBlockDate } from "./due-date";
import { IssueBlockLabels } from "./labels";
import { IssueBlockMembers } from "./member";
import { IssueBlockModules } from "./modules";
import { IssueBlockPriority } from "./priority";
import { IssueBlockState } from "./state";
export interface IIssueProperties {
issue: IIssue;
displayProperties: IIssueDisplayProperties | undefined;
className: string;
}
export const IssueProperties = observer(function IssueProperties(props: IIssueProperties) {
const { issue, displayProperties, className } = props;
if (!displayProperties || !issue.project_id) return null;
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
return (
<div className={className}>
{/* basic properties */}
{/* state */}
{issue.state_id && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="state">
<div className="h-5">
<IssueBlockState stateId={issue.state_id} />
</div>
</WithDisplayPropertiesHOC>
)}
{/* priority */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="priority">
<div className="h-5">
<IssueBlockPriority priority={issue.priority} />
</div>
</WithDisplayPropertiesHOC>
{/* label */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
<div className="h-5">
<IssueBlockLabels labelIds={issue.label_ids} />
</div>
</WithDisplayPropertiesHOC>
{/* start date */}
{issue?.start_date && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
<div className="h-5">
<IssueBlockDate
due_date={issue?.start_date}
stateId={issue?.state_id ?? undefined}
shouldHighLight={false}
/>
</div>
</WithDisplayPropertiesHOC>
)}
{/* target/due date */}
{issue?.target_date && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
<div className="h-5">
<IssueBlockDate due_date={issue?.target_date} stateId={issue?.state_id ?? undefined} />
</div>
</WithDisplayPropertiesHOC>
)}
{/* assignee */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="assignee">
<div className="h-5">
<IssueBlockMembers memberIds={issue.assignee_ids} />
</div>
</WithDisplayPropertiesHOC>
{/* modules */}
{issue.module_ids && issue.module_ids.length > 0 && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
<div className="h-5">
<IssueBlockModules moduleIds={issue.module_ids} />
</div>
</WithDisplayPropertiesHOC>
)}
{/* cycles */}
{issue.cycle_id && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
<div className="h-5">
<IssueBlockCycle cycleId={issue.cycle_id} />
</div>
</WithDisplayPropertiesHOC>
)}
{/* estimates */}
{/* {projectId && areEstimateEnabledByProjectId(projectId?.toString()) && (
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
<div className="h-5">
<EstimateDropdown
value={issue.estimate_point ?? undefined}
onChange={handleEstimate}
projectId={issue.project_id}
disabled={isReadOnly}
buttonVariant="border-with-text"
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
)} */}
{/* extra render properties */}
{/* sub-issues */}
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="sub_issue_count"
shouldRenderProperty={(properties) => !!properties.sub_issue_count && !!issue.sub_issues_count}
>
<Tooltip tooltipHeading="Sub-work items" tooltipContent={`${issue.sub_issues_count}`}>
<div
className={cn(
"flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1",
{
"cursor-pointer hover:bg-layer-1": issue.sub_issues_count,
}
)}
>
<ViewsIcon className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-11">{issue.sub_issues_count}</div>
</div>
</Tooltip>
</WithDisplayPropertiesHOC>
{/* attachments */}
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="attachment_count"
shouldRenderProperty={(properties) => !!properties.attachment_count && !!issue.attachment_count}
>
<Tooltip tooltipHeading="Attachments" tooltipContent={`${issue.attachment_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1">
<Paperclip className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-11">{issue.attachment_count}</div>
</div>
</Tooltip>
</WithDisplayPropertiesHOC>
{/* link */}
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey="link"
shouldRenderProperty={(properties) => !!properties.link && !!issue.link_count}
>
<Tooltip tooltipHeading="Links" tooltipContent={`${issue.link_count}`}>
<div className="flex h-5 flex-shrink-0 items-center justify-center gap-2 overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1">
<LinkIcon className="h-3 w-3 flex-shrink-0" strokeWidth={2} />
<div className="text-11">{issue.link_count}</div>
</div>
</Tooltip>
</WithDisplayPropertiesHOC>
</div>
);
});
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane ui
import { CycleIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useCycle } from "@/hooks/store/use-cycle";
type Props = {
cycleId: string | undefined;
shouldShowBorder?: boolean;
};
export const IssueBlockCycle = observer(function IssueBlockCycle({ cycleId, shouldShowBorder = true }: Props) {
const { getCycleById } = useCycle();
const cycle = getCycleById(cycleId);
return (
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? "No Cycle"}>
<div
className={cn(
"flex h-full w-full items-center justify-between gap-1 rounded-sm px-2.5 py-1 text-11 duration-300 focus:outline-none",
{ "border-[0.5px] border-strong": shouldShowBorder }
)}
>
<div className="flex w-full items-center gap-1.5 text-11">
<CycleIcon className="h-3 w-3 flex-shrink-0" />
<div className="max-w-40 truncate">{cycle?.name ?? "No Cycle"}</div>
</div>
</div>
</Tooltip>
);
});
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { DueDatePropertyIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
// hooks
import { useStates } from "@/hooks/store/use-state";
type Props = {
due_date: string | undefined;
stateId: string | undefined;
shouldHighLight?: boolean;
shouldShowBorder?: boolean;
};
export const IssueBlockDate = observer(function IssueBlockDate(props: Props) {
const { due_date, stateId, shouldHighLight = true, shouldShowBorder = true } = props;
const { getStateById } = useStates();
const state = getStateById(stateId);
const formattedDate = renderFormattedDate(due_date);
return (
<Tooltip tooltipHeading="Due Date" tooltipContent={formattedDate}>
<div
className={cn("flex h-full items-center gap-1 rounded-sm px-2.5 py-1 text-11 text-primary", {
"text-danger-primary": shouldHighLight && due_date && shouldHighlightIssueDueDate(due_date, state?.group),
"border-[0.5px] border-strong": shouldShowBorder,
})}
>
<DueDatePropertyIcon className="size-3 flex-shrink-0" />
{formattedDate ? formattedDate : "No Date"}
</div>
</Tooltip>
);
});
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { LabelPropertyIcon } from "@plane/propel/icons";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { useLabel } from "@/hooks/store/use-label";
type Props = {
labelIds: string[];
shouldShowLabel?: boolean;
};
export const IssueBlockLabels = observer(function IssueBlockLabels({ labelIds, shouldShowLabel = false }: Props) {
const { getLabelsByIds } = useLabel();
const labels = getLabelsByIds(labelIds);
const labelsString = labels.length > 0 ? labels.map((label) => label.name).join(", ") : "No Labels";
if (labels.length <= 0)
return (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
<div
className={`flex h-full items-center justify-center gap-2 rounded-sm border-[0.5px] border-strong px-2.5 py-1 text-11`}
>
<LabelPropertyIcon className="h-3.5 w-3.5" strokeWidth={2} />
{shouldShowLabel && <span>No Labels</span>}
</div>
</Tooltip>
);
return (
<div className="flex h-5 w-full flex-wrap items-center gap-2 overflow-hidden">
{labels.length <= 2 ? (
<>
{labels.map((label) => (
<Tooltip key={label.id} position="top" tooltipHeading="Labels" tooltipContent={label?.name ?? ""}>
<div
key={label?.id}
className={`flex h-full max-w-full flex-shrink-0 items-center overflow-hidden rounded-sm border-[0.5px] border-strong px-2.5 py-1 text-11`}
>
<div className="flex max-w-full items-center gap-1.5 overflow-hidden text-secondary">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color ?? "#000000",
}}
/>
<div className="line-clamp-1 inline-block w-auto max-w-[100px] truncate">{label?.name}</div>
</div>
</div>
</Tooltip>
))}
</>
) : (
<div
className={`cursor-not-allowed" flex h-full flex-shrink-0 items-center rounded-sm border-[0.5px] border-strong px-2.5 py-1 text-11`}
>
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={labelsString}>
<div className="flex h-full items-center gap-1.5 text-secondary">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-accent-primary" />
{`${labels.length} Labels`}
</div>
</Tooltip>
</div>
)}
</div>
);
});
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// icons
import type { LucideIcon } from "lucide-react";
import { MembersPropertyIcon } from "@plane/propel/icons";
// plane ui
import { Avatar, AvatarGroup } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
//
import type { TPublicMember } from "@/types/member";
type Props = {
memberIds: string[];
shouldShowBorder?: boolean;
};
type AvatarProps = {
showTooltip: boolean;
members: TPublicMember[];
icon?: LucideIcon;
};
export const ButtonAvatars = observer(function ButtonAvatars(props: AvatarProps) {
const { showTooltip, members, icon: Icon } = props;
if (Array.isArray(members)) {
if (members.length > 1) {
return (
<AvatarGroup size="md" showTooltip={!showTooltip}>
{members.map((member) => {
if (!member) return;
return <Avatar key={member.id} src={member.member__avatar} name={member.member__display_name} />;
})}
</AvatarGroup>
);
} else if (members.length === 1) {
return (
<Avatar
src={members[0].member__avatar}
name={members[0].member__display_name}
size="md"
showTooltip={!showTooltip}
/>
);
}
}
return Icon ? (
<Icon className="h-3 w-3 flex-shrink-0" />
) : (
<MembersPropertyIcon className="mx-[4px] h-3 w-3 flex-shrink-0" />
);
});
export const IssueBlockMembers = observer(function IssueBlockMembers({ memberIds, shouldShowBorder = true }: Props) {
const { getMembersByIds } = useMember();
const members = getMembersByIds(memberIds);
return (
<div className="relative flex h-full flex-wrap items-center gap-1">
<div
className={cn("flex flex-shrink-0 cursor-default items-center rounded-md text-11", {
"border-[0.5px] border-strong px-2.5 py-1": shouldShowBorder && !members?.length,
})}
>
<div className="flex items-center gap-1.5 text-secondary">
<ButtonAvatars members={members} showTooltip={false} />
{!shouldShowBorder && members.length <= 1 && (
<span>{members?.[0]?.member__display_name ?? "No Assignees"}</span>
)}
</div>
</div>
</div>
);
});
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane ui
import { ModuleIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
// hooks
import { useModule } from "@/hooks/store/use-module";
type Props = {
moduleIds: string[] | undefined;
shouldShowBorder?: boolean;
};
export const IssueBlockModules = observer(function IssueBlockModules({ moduleIds, shouldShowBorder = true }: Props) {
const { getModulesByIds } = useModule();
const modules = getModulesByIds(moduleIds ?? []);
const modulesString = modules.map((module) => module.name).join(", ");
return (
<div className="relative flex h-full flex-wrap items-center gap-1">
<Tooltip tooltipHeading="Modules" tooltipContent={modulesString}>
{modules.length <= 1 ? (
<div
key={modules?.[0]?.id}
className={cn("flex h-full flex-shrink-0 cursor-default items-center rounded-md px-2.5 py-1 text-11", {
"border-[0.5px] border-strong": shouldShowBorder,
})}
>
<div className="flex items-center gap-1.5 text-secondary">
<ModuleIcon className="h-3 w-3 flex-shrink-0" />
<div className="text-11">{modules?.[0]?.name ?? "No Modules"}</div>
</div>
</div>
) : (
<div className="flex h-full flex-shrink-0 cursor-default items-center rounded-md border border-strong px-2.5 py-1 text-11">
<div className="flex items-center gap-1.5 text-secondary">
<div className="text-11">{modules.length} Modules</div>
</div>
</div>
)}
</Tooltip>
</div>
);
});
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// types
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { TIssuePriorities } from "@plane/types";
// constants
import { cn, getIssuePriorityFilters } from "@plane/utils";
export function IssueBlockPriority({
priority,
shouldShowName = false,
}: {
priority: TIssuePriorities | null;
shouldShowName?: boolean;
}) {
// hooks
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
const priorityClasses = {
urgent: "bg-layer-2 text-priority-urgent border-priority-urgent px-1",
high: "bg-layer-2 text-priority-high border-priority-high",
medium: "bg-layer-2 text-priority-medium border-priority-medium",
low: "bg-layer-2 text-priority-low border-priority-low",
none: "bg-layer-2 text-priority-none border-priority-none",
};
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<div
className={cn(
"flex h-full items-center gap-1.5 rounded-sm border-[0.5px] px-2 py-0.5 text-11",
priorityClasses[priority ?? "none"],
{
// compact the icons if text is hidden
"px-0.5": !shouldShowName,
// highlight the whole button if text is hidden and priority is urgent
"border-priority-urgent": priority === "urgent" && shouldShowName,
}
)}
>
{priority ? (
<PriorityIcon
priority={priority}
size={12}
className={cn("flex-shrink-0", {
// increase the icon size if text is hidden
"h-3.5 w-3.5": !shouldShowName,
// centre align the icons if text is hidden
"translate-x-[0.0625rem]": !shouldShowName && priority === "high",
"translate-x-0.5": !shouldShowName && priority === "medium",
"translate-x-1": !shouldShowName && priority === "low",
// highlight the icon if priority is urgent
})}
/>
) : (
<SignalHigh className="size-3" />
)}
{shouldShowName && <span className="pl-2 text-13">{t(priority_detail?.titleTranslationKey || "")}</span>}
</div>
</Tooltip>
);
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
// plane ui
import { StateGroupIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import type { TStateGroups } from "@plane/types";
// plane utils
import { cn } from "@plane/utils";
//hooks
import { useStates } from "@/hooks/store/use-state";
type Props = {
shouldShowBorder?: boolean;
} & (
| {
stateDetails: {
name: string;
group: TStateGroups;
};
}
| {
stateId: string;
}
);
export const IssueBlockState = observer(function IssueBlockState(props: Props) {
const { shouldShowBorder = true } = props;
// store hooks
const { getStateById } = useStates();
// derived values
const state = "stateId" in props ? getStateById(props.stateId) : props.stateDetails;
if (!state) return null;
return (
<Tooltip tooltipHeading="State" tooltipContent={state.name}>
<div
className={cn("flex h-full w-full items-center justify-between gap-1 rounded-sm px-2.5 py-1 text-11", {
"border-[0.5px] border-strong": shouldShowBorder,
})}
>
<div className="flex w-full items-center gap-1.5">
<StateGroupIcon stateGroup={state.group} />
<div className="text-11">{state.name}</div>
</div>
</div>
</Tooltip>
);
});