Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled

Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
chuan
2025-11-07 00:00:52 +08:00
commit 8ebde8aa05
4886 changed files with 462270 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
"use client";
import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
import { Pencil, Trash, Link as LinkIcon } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
import type { TIssue, TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { ControlLink, CustomMenu } from "@plane/ui";
import { generateWorkItemLink } from "@plane/utils";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useProject } from "@/hooks/store/use-project";
import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web imports
import { IssueIdentifier } from "@/plane-web/components/issues/issue-details/issue-identifier";
import type { TIssueRelationTypes } from "@/plane-web/types";
// local imports
import { useRelationOperations } from "../issue-detail-widgets/relations/helper";
import { RelationIssueProperty } from "./properties";
type Props = {
workspaceSlug: string;
issueId: string;
relationKey: TIssueRelationTypes;
relationIssueId: string;
disabled: boolean;
handleIssueCrudState: (
key: "update" | "delete" | "removeRelation",
issueId: string,
issue?: TIssue | null,
relationKey?: TIssueRelationTypes | null,
relationIssueId?: string | null
) => void;
issueServiceType?: TIssueServiceType;
};
export const RelationIssueListItem: FC<Props> = observer((props) => {
const {
workspaceSlug,
issueId,
relationKey,
relationIssueId,
disabled = false,
handleIssueCrudState,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
const { t } = useTranslation();
// store hooks
const {
issue: { getIssueById },
removeRelation,
toggleCreateIssueModal,
toggleDeleteIssueModal,
} = useIssueDetail(issueServiceType);
const project = useProject();
const { isMobile } = usePlatformOS();
// derived values
const issue = getIssueById(relationIssueId);
const { handleRedirection } = useIssuePeekOverviewRedirection(!!issue?.is_epic);
const issueOperations = useRelationOperations(!!issue?.is_epic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES);
const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined;
const projectId = issue?.project_id;
if (!issue || !projectId) return <></>;
const workItemLink = generateWorkItemLink({
workspaceSlug: workspaceSlug.toString(),
projectId: issue?.project_id,
issueId: issue?.id,
projectIdentifier: projectDetail?.identifier,
sequenceId: issue?.sequence_id,
isEpic: issue?.is_epic,
});
// handlers
const handleIssuePeekOverview = (issue: TIssue) => {
if (issue.is_epic) {
// open epics in new tab
window.open(workItemLink, "_blank");
return;
}
handleRedirection(workspaceSlug, issue, isMobile);
};
const handleEditIssue = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
handleIssueCrudState("update", relationIssueId, { ...issue });
toggleCreateIssueModal(true);
};
const handleDeleteIssue = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
handleIssueCrudState("delete", relationIssueId, issue);
toggleDeleteIssueModal(relationIssueId);
handleIssueCrudState("removeRelation", issueId, issue, relationKey, relationIssueId);
};
const handleCopyIssueLink = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
e.preventDefault();
issueOperations.copyLink(workItemLink);
};
const handleRemoveRelation = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
removeRelation(workspaceSlug, projectId, issueId, relationKey, relationIssueId);
};
return (
<div key={relationIssueId}>
<ControlLink
id={`issue-${issue.id}`}
href={workItemLink}
onClick={() => handleIssuePeekOverview(issue)}
className="w-full cursor-pointer"
>
{issue && (
<div className="group relative flex min-h-11 h-full w-full items-center px-1.5 py-1 transition-all hover:bg-custom-background-90">
<span className="size-5 flex-shrink-0" />
<div className="flex w-full truncate cursor-pointer items-center gap-3">
<div className="flex-shrink-0">
{projectDetail && (
<IssueIdentifier
projectId={projectDetail.id}
issueTypeId={issue.type_id}
projectIdentifier={projectDetail.identifier}
issueSequenceId={issue.sequence_id}
textContainerClassName="text-xs text-custom-text-200"
/>
)}
</div>
<Tooltip tooltipContent={issue.name} isMobile={isMobile}>
<span className="w-full truncate text-sm text-custom-text-100">{issue.name}</span>
</Tooltip>
</div>
<div
className="flex-shrink-0 text-sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<RelationIssueProperty
workspaceSlug={workspaceSlug}
issueId={relationIssueId}
disabled={disabled}
issueOperations={issueOperations}
issueServiceType={issueServiceType}
/>
</div>
<div className="flex-shrink-0 text-sm">
<CustomMenu placement="bottom-end" ellipsis>
{!disabled && (
<CustomMenu.MenuItem onClick={handleEditIssue}>
<div className="flex items-center gap-2">
<Pencil className="h-3.5 w-3.5" strokeWidth={2} />
<span>{t("common.actions.edit")}</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
<div className="flex items-center gap-2">
<LinkIcon className="h-3.5 w-3.5" strokeWidth={2} />
<span>{t("common.actions.copy_link")}</span>
</div>
</CustomMenu.MenuItem>
{!disabled && (
<CustomMenu.MenuItem onClick={handleRemoveRelation}>
<div className="flex items-center gap-2">
<CloseIcon className="h-3.5 w-3.5" strokeWidth={2} />
<span>{t("common.actions.remove_relation")}</span>
</div>
</CustomMenu.MenuItem>
)}
{!disabled && (
<CustomMenu.MenuItem onClick={handleDeleteIssue}>
<div className="flex items-center gap-2">
<Trash className="h-3.5 w-3.5" strokeWidth={2} />
<span>{t("common.actions.delete")}</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
</div>
)}
</ControlLink>
</div>
);
});

View File

@@ -0,0 +1,58 @@
"use client";
import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
// plane imports
import type { TIssue, TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
// Plane-web imports
import type { TIssueRelationTypes } from "@/plane-web/types";
// local imports
import { RelationIssueListItem } from "./issue-list-item";
type Props = {
workspaceSlug: string;
issueId: string;
issueIds: string[];
relationKey: TIssueRelationTypes;
handleIssueCrudState: (
key: "update" | "delete" | "removeRelation",
issueId: string,
issue?: TIssue | null,
relationKey?: TIssueRelationTypes | null,
relationIssueId?: string | null
) => void;
disabled?: boolean;
issueServiceType?: TIssueServiceType;
};
export const RelationIssueList: FC<Props> = observer((props) => {
const {
workspaceSlug,
issueId,
issueIds,
relationKey,
disabled = false,
handleIssueCrudState,
issueServiceType = EIssueServiceType.ISSUES,
} = props;
return (
<div className="relative">
{issueIds &&
issueIds.length > 0 &&
issueIds.map((relationIssueId) => (
<RelationIssueListItem
key={relationIssueId}
workspaceSlug={workspaceSlug}
issueId={issueId}
relationKey={relationKey}
relationIssueId={relationIssueId}
disabled={disabled}
handleIssueCrudState={handleIssueCrudState}
issueServiceType={issueServiceType}
/>
))}
</div>
);
});

View File

@@ -0,0 +1,91 @@
"use client";
import type { FC } from "react";
import React from "react";
import { observer } from "mobx-react";
// components
import type { TIssuePriorities, TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { PriorityDropdown } from "@/components/dropdowns/priority";
import { StateDropdown } from "@/components/dropdowns/state/dropdown";
// hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
// types
import type { TRelationIssueOperations } from "../issue-detail-widgets/relations/helper";
type Props = {
workspaceSlug: string;
issueId: string;
disabled: boolean;
issueOperations: TRelationIssueOperations;
issueServiceType?: TIssueServiceType;
};
export const RelationIssueProperty: FC<Props> = observer((props) => {
const { workspaceSlug, issueId, disabled, issueOperations, issueServiceType = EIssueServiceType.ISSUES } = props;
// hooks
const {
issue: { getIssueById },
} = useIssueDetail(issueServiceType);
// derived value
const issue = getIssueById(issueId);
// if issue is not found, return empty
if (!issue) return <></>;
// handlers
const handleStateChange = (val: string) =>
issue.project_id &&
issueOperations.update(workspaceSlug, issue.project_id, issueId, {
state_id: val,
});
const handlePriorityChange = (val: TIssuePriorities) =>
issue.project_id &&
issueOperations.update(workspaceSlug, issue.project_id, issueId, {
priority: val,
});
const handleAssigneeChange = (val: string[]) =>
issue.project_id &&
issueOperations.update(workspaceSlug, issue.project_id, issueId, {
assignee_ids: val,
});
return (
<div className="relative flex items-center gap-2">
<div className="h-5 flex-shrink-0">
<StateDropdown
value={issue.state_id}
projectId={issue.project_id ?? undefined}
onChange={handleStateChange}
disabled={disabled}
buttonVariant="border-with-text"
/>
</div>
<div className="h-5 flex-shrink-0">
<PriorityDropdown
value={issue.priority}
onChange={handlePriorityChange}
disabled={disabled}
buttonVariant="border-without-text"
buttonClassName="border"
/>
</div>
<div className="h-5 flex-shrink-0">
<MemberDropdown
value={issue.assignee_ids}
projectId={issue.project_id ?? undefined}
onChange={handleAssigneeChange}
disabled={disabled}
multiple
buttonVariant={(issue?.assignee_ids || []).length > 0 ? "transparent-without-text" : "border-without-text"}
buttonClassName={(issue?.assignee_ids || []).length > 0 ? "hover:bg-transparent px-0" : ""}
/>
</div>
</div>
);
});