mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
Merge branch 'sync/ce-ee' of github.com:makeplane/plane-ee into develop
This commit is contained in:
@@ -45,7 +45,7 @@ export const IssueLink = ({ activity }: { activity: IIssueActivity }) => {
|
||||
}`}`}
|
||||
target={activity.issue === null ? "_self" : "_blank"}
|
||||
rel={activity.issue === null ? "" : "noopener noreferrer"}
|
||||
className="inline-flex items-center gap-1 font-medium text-custom-text-100 hover:underline"
|
||||
className="font-medium text-custom-text-100 hover:underline"
|
||||
>
|
||||
<span className="whitespace-nowrap">{`${activity.project_detail.identifier}-${activity.issue_detail.sequence_id}`}</span>{" "}
|
||||
<span className="font-normal">{activity.issue_detail?.name}</span>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ArchiveCycleModal: React.FC<Props> = (props) => {
|
||||
message: "Your archives can be found in project archives.",
|
||||
});
|
||||
onClose();
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles?peekCycle=${cycleId}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CustomMenu } from "@plane/ui";
|
||||
import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember } from "@/hooks/store";
|
||||
import { useIssues, useCycle, useProjectState, useLabel, useMember, useProject } from "@/hooks/store";
|
||||
|
||||
export const CycleMobileHeader = () => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
@@ -24,6 +24,7 @@ export const CycleMobileHeader = () => {
|
||||
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
@@ -151,6 +152,8 @@ export const CycleMobileHeader = () => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
@@ -174,6 +177,8 @@ export const CycleMobileHeader = () => {
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["cycle"]}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const CycleQuickActions: React.FC<Props> = observer((props) => {
|
||||
title: "Restore success",
|
||||
message: "Your cycle can be found in project cycles.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -129,7 +129,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
title: "Restore success",
|
||||
message: "Your cycle can be found in project cycles.",
|
||||
});
|
||||
router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/cycles/${cycleId}`);
|
||||
router.push(`/${workspaceSlug.toString()}/projects/${projectId.toString()}/archives/cycles`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -149,7 +149,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{isClearable && !disabled && isDateSelected && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
className={cn("h-2.5 w-2.5 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onChange(null);
|
||||
|
||||
@@ -250,6 +250,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
@@ -262,6 +264,8 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["cycle"]}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
|
||||
|
||||
@@ -252,6 +252,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
@@ -264,6 +266,8 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["module"]}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
@@ -152,6 +154,8 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -192,6 +192,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
@@ -203,6 +205,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -210,6 +210,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
@@ -221,6 +223,8 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
{canUserCreateIssue && (
|
||||
|
||||
@@ -57,6 +57,7 @@ export const ProjectsHeader = observer(() => {
|
||||
if (Array.isArray(value))
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
|
||||
@@ -39,7 +39,14 @@ export const ArchiveIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
setIsArchiving(true);
|
||||
await onSubmit()
|
||||
.then(() => onClose())
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Archive success",
|
||||
message: "Your archives can be found in project archives.",
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
||||
@@ -9,13 +9,14 @@ import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/com
|
||||
// constants
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useIssues, useLabel, useMember, useProjectState } from "@/hooks/store";
|
||||
import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
|
||||
export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
@@ -89,6 +90,8 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
|
||||
}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,11 @@ type TIssueActivityCommentRoot = {
|
||||
issueId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
showAccessSpecifier?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer((props) => {
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier } = props;
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier, disabled } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityCommentByIssueId },
|
||||
@@ -37,6 +38,7 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
|
||||
activityOperations={activityOperations}
|
||||
ends={index === 0 ? "top" : index === activityComments.length - 1 ? "bottom" : undefined}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : activityComment.activity_type === "ACTIVITY" ? (
|
||||
<IssueActivityList
|
||||
|
||||
@@ -25,10 +25,11 @@ type TIssueCommentCard = {
|
||||
activityOperations: TActivityOperations;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
showAccessSpecifier?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
const { workspaceSlug, commentId, activityOperations, ends, showAccessSpecifier = false } = props;
|
||||
const { workspaceSlug, commentId, activityOperations, ends, showAccessSpecifier = false, disabled = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentById },
|
||||
@@ -81,7 +82,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
commentId={commentId}
|
||||
quickActions={
|
||||
<>
|
||||
{currentUser?.id === comment.actor && (
|
||||
{!disabled && currentUser?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
@@ -184,6 +185,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = (props) => {
|
||||
projectId={comment?.project_detail?.id}
|
||||
commentId={comment.id}
|
||||
currentUser={currentUser}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -12,10 +12,11 @@ type TIssueCommentRoot = {
|
||||
issueId: string;
|
||||
activityOperations: TActivityOperations;
|
||||
showAccessSpecifier?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier } = props;
|
||||
const { workspaceSlug, issueId, activityOperations, showAccessSpecifier, disabled } = props;
|
||||
// hooks
|
||||
const {
|
||||
comment: { getCommentsByIssueId },
|
||||
@@ -34,6 +35,7 @@ export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||
ends={index === 0 ? "top" : index === commentIds.length - 1 ? "bottom" : undefined}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ type TIssueActivity = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type TActivityTabs = "all" | "activity" | "comments";
|
||||
@@ -43,7 +44,7 @@ export type TActivityOperations = {
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId } = props;
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// hooks
|
||||
const { createComment, updateComment, removeComment } = useIssueDetail();
|
||||
const { getProjectById } = useProject();
|
||||
@@ -145,12 +146,15 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
issueId={issueId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : activityTab === "activity" ? (
|
||||
<IssueActivityRoot issueId={issueId} />
|
||||
@@ -161,12 +165,15 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||
issueId={issueId}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
{!disabled && (
|
||||
<IssueCommentCreate
|
||||
workspaceSlug={workspaceSlug}
|
||||
activityOperations={activityOperations}
|
||||
showAccessSpecifier={project.is_deployed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -132,7 +132,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
import { Pencil, X } from "lucide-react";
|
||||
// hooks
|
||||
// components
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
import { ParentIssuesListModal } from "@/components/issues";
|
||||
// ui
|
||||
// helpers
|
||||
@@ -30,7 +30,13 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { isParentIssueModalOpen, toggleParentIssueModal } = useIssueDetail();
|
||||
const {
|
||||
isParentIssueModalOpen,
|
||||
toggleParentIssueModal,
|
||||
removeSubIssue,
|
||||
subIssues: { setSubIssueHelpers },
|
||||
} = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined;
|
||||
@@ -47,6 +53,25 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSubIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
parentIssueId: string,
|
||||
issueId: string
|
||||
) => {
|
||||
try {
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
await removeSubIssue(workspaceSlug, projectId, parentIssueId, issueId);
|
||||
setSubIssueHelpers(parentIssueId, "issue_loader", issueId);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
return (
|
||||
@@ -92,7 +117,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleParentIssue(null);
|
||||
handleRemoveSubIssue(workspaceSlug, projectId, parentIssue.id, issueId);
|
||||
}}
|
||||
>
|
||||
<X className="h-2.5 w-2.5 text-custom-text-300 hover:text-red-500" />
|
||||
|
||||
@@ -3,10 +3,12 @@ import { observer } from "mobx-react-lite";
|
||||
import { IUser } from "@plane/types";
|
||||
// components
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// helper
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { formatTextList } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
// helper
|
||||
// types
|
||||
import { ReactionSelector } from "./reaction-selector";
|
||||
|
||||
@@ -15,10 +17,11 @@ export type TIssueCommentReaction = {
|
||||
projectId: string;
|
||||
commentId: string;
|
||||
currentUser: IUser;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, commentId, currentUser } = props;
|
||||
const { workspaceSlug, projectId, commentId, currentUser, disabled = false } = props;
|
||||
|
||||
// hooks
|
||||
const {
|
||||
@@ -88,12 +91,14 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
||||
|
||||
return (
|
||||
<div className="mt-4 relative flex items-center gap-1.5">
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={userReactions}
|
||||
onSelect={issueCommentReactionOperations.react}
|
||||
/>
|
||||
{!disabled && (
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={userReactions}
|
||||
onSelect={issueCommentReactionOperations.react}
|
||||
/>
|
||||
)}
|
||||
|
||||
{reactionIds &&
|
||||
Object.keys(reactionIds || {}).map(
|
||||
@@ -103,11 +108,15 @@ export const IssueCommentReaction: FC<TIssueCommentReaction> = observer((props)
|
||||
<Tooltip tooltipContent={getReactionUsers(reaction)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => issueCommentReactionOperations.react(reaction)}
|
||||
onClick={() => !disabled && issueCommentReactionOperations.react(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100",
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80",
|
||||
{
|
||||
"cursor-not-allowed": disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IUser } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { renderEmoji } from "@/helpers/emoji.helper";
|
||||
import { formatTextList } from "@/helpers/issue.helper";
|
||||
import { useIssueDetail, useMember } from "@/hooks/store";
|
||||
@@ -16,10 +17,11 @@ export type TIssueReaction = {
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
currentUser: IUser;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, currentUser } = props;
|
||||
const { workspaceSlug, projectId, issueId, currentUser, disabled = false } = props;
|
||||
// hooks
|
||||
const {
|
||||
reaction: { getReactionsByIssueId, reactionsByUser, getReactionById },
|
||||
@@ -89,7 +91,9 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="mt-4 relative flex items-center gap-1.5">
|
||||
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
|
||||
{!disabled && (
|
||||
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
|
||||
)}
|
||||
|
||||
{reactionIds &&
|
||||
Object.keys(reactionIds || {}).map(
|
||||
@@ -99,11 +103,15 @@ export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||
<Tooltip tooltipContent={getReactionUsers(reaction)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => issueReactionOperations.react(reaction)}
|
||||
onClick={() => !disabled && issueReactionOperations.react(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||
}`}
|
||||
className={cn(
|
||||
"flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100",
|
||||
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80",
|
||||
{
|
||||
"cursor-not-allowed": disabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||
|
||||
@@ -144,22 +144,12 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issue archived successfully.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue details page" },
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be archived. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue details page" },
|
||||
|
||||
@@ -82,9 +82,11 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
const [isExistingIssueModalOpen, setIsExistingIssueModalOpen] = useState(false);
|
||||
// derived values
|
||||
const projectDetail = projectId ? getProjectById(projectId.toString()) : null;
|
||||
const ExistingIssuesListModalPayload = moduleId
|
||||
? { module: moduleId.toString(), target_date: "none" }
|
||||
: { cycle: true, target_date: "none" };
|
||||
const ExistingIssuesListModalPayload = addIssuesToView
|
||||
? moduleId
|
||||
? { module: moduleId.toString(), target_date: "none" }
|
||||
: { cycle: true, target_date: "none" }
|
||||
: { target_date: "none" };
|
||||
|
||||
const {
|
||||
reset,
|
||||
@@ -234,33 +236,22 @@ export const CalendarQuickAddIssueForm: React.FC<Props> = observer((props) => {
|
||||
block: isMenuOpen,
|
||||
})}
|
||||
>
|
||||
{addIssuesToView ? (
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
menuButtonOnClick={() => setIsMenuOpen(true)}
|
||||
onMenuClose={() => setIsMenuOpen(false)}
|
||||
className="w-full"
|
||||
customButtonClassName="w-full"
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100">
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
|
||||
<span className="text-sm font-medium flex-shrink-0 text-custom-primary-100">New Issue</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleNewIssue}>New Issue</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100"
|
||||
onClick={handleNewIssue}
|
||||
>
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||
</button>
|
||||
)}
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
menuButtonOnClick={() => setIsMenuOpen(true)}
|
||||
onMenuClose={() => setIsMenuOpen(false)}
|
||||
className="w-full"
|
||||
customButtonClassName="w-full"
|
||||
customButton={
|
||||
<div className="flex w-full items-center gap-x-[6px] rounded-md px-2 py-1.5 text-custom-primary-100">
|
||||
<PlusIcon className="h-3.5 w-3.5 stroke-2 flex-shrink-0" />
|
||||
<span className="text-sm font-medium flex-shrink-0 text-custom-primary-100">New Issue</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={handleNewIssue}>New Issue</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleExistingIssue}>Add existing issue</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -21,6 +21,8 @@ type Props = {
|
||||
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
|
||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
|
||||
cycleViewDisabled?: boolean;
|
||||
moduleViewDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
@@ -31,17 +33,32 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
handleDisplayPropertiesUpdate,
|
||||
layoutDisplayFiltersOptions,
|
||||
ignoreGroupedFilters = [],
|
||||
cycleViewDisabled = false,
|
||||
moduleViewDisabled = false,
|
||||
} = props;
|
||||
|
||||
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
|
||||
Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter);
|
||||
|
||||
const computedIgnoreGroupedFilters: Partial<TIssueGroupByOptions>[] = [];
|
||||
if (cycleViewDisabled) {
|
||||
ignoreGroupedFilters.push("cycle");
|
||||
}
|
||||
if (moduleViewDisabled) {
|
||||
ignoreGroupedFilters.push("module");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vertical-scrollbar scrollbar-sm relative h-full w-full divide-y divide-custom-border-200 overflow-hidden overflow-y-auto px-2.5">
|
||||
{/* display properties */}
|
||||
{layoutDisplayFiltersOptions?.display_properties && (
|
||||
<div className="py-2">
|
||||
<FilterDisplayProperties displayProperties={displayProperties} handleUpdate={handleDisplayPropertiesUpdate} />
|
||||
<FilterDisplayProperties
|
||||
displayProperties={displayProperties}
|
||||
handleUpdate={handleDisplayPropertiesUpdate}
|
||||
cycleViewDisabled={cycleViewDisabled}
|
||||
moduleViewDisabled={moduleViewDisabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,7 +73,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
group_by: val,
|
||||
})
|
||||
}
|
||||
ignoreGroupedFilters={ignoreGroupedFilters}
|
||||
ignoreGroupedFilters={[...ignoreGroupedFilters, ...computedIgnoreGroupedFilters]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -74,7 +91,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
|
||||
})
|
||||
}
|
||||
subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []}
|
||||
ignoreGroupedFilters={ignoreGroupedFilters}
|
||||
ignoreGroupedFilters={[...ignoreGroupedFilters, ...computedIgnoreGroupedFilters]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,13 +10,22 @@ import { FilterHeader } from "../helpers/filter-header";
|
||||
type Props = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
handleUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
|
||||
cycleViewDisabled?: boolean;
|
||||
moduleViewDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
|
||||
const { displayProperties, handleUpdate } = props;
|
||||
const { displayProperties, handleUpdate, cycleViewDisabled = false, moduleViewDisabled = false } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||
|
||||
// Filter out "cycle" and "module" keys if cycleViewDisabled or moduleViewDisabled is true
|
||||
const filteredDisplayProperties = ISSUE_DISPLAY_PROPERTIES.filter((property) => {
|
||||
if (cycleViewDisabled && property.key === "cycle") return false;
|
||||
if (moduleViewDisabled && property.key === "modules") return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
@@ -26,23 +35,25 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
|
||||
<button
|
||||
key={displayProperty.key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-0.5 text-xs transition-all ${
|
||||
displayProperties?.[displayProperty.key]
|
||||
? "border-custom-primary-100 bg-custom-primary-100 text-white"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleUpdate({
|
||||
[displayProperty.key]: !displayProperties?.[displayProperty.key],
|
||||
})
|
||||
}
|
||||
>
|
||||
{displayProperty.title}
|
||||
</button>
|
||||
{filteredDisplayProperties.map((displayProperty) => (
|
||||
<>
|
||||
<button
|
||||
key={displayProperty.key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-0.5 text-xs transition-all ${
|
||||
displayProperties?.[displayProperty.key]
|
||||
? "border-custom-primary-100 bg-custom-primary-100 text-white"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleUpdate({
|
||||
[displayProperty.key]: !displayProperties?.[displayProperty.key],
|
||||
})
|
||||
}
|
||||
>
|
||||
{displayProperty.title}
|
||||
</button>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -30,10 +30,21 @@ type Props = {
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
memberIds?: string[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
cycleViewDisabled?: boolean;
|
||||
moduleViewDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
const { filters, handleFiltersUpdate, layoutDisplayFiltersOptions, labels, memberIds, states } = props;
|
||||
const {
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
layoutDisplayFiltersOptions,
|
||||
labels,
|
||||
memberIds,
|
||||
states,
|
||||
cycleViewDisabled = false,
|
||||
moduleViewDisabled = false,
|
||||
} = props;
|
||||
// hooks
|
||||
const {
|
||||
router: { moduleId, cycleId },
|
||||
@@ -111,7 +122,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* cycle */}
|
||||
{isFilterEnabled("cycle") && !cycleId && (
|
||||
{isFilterEnabled("cycle") && !cycleId && !cycleViewDisabled && (
|
||||
<div className="py-2">
|
||||
<FilterCycle
|
||||
appliedFilters={filters.cycle ?? null}
|
||||
@@ -122,7 +133,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* module */}
|
||||
{isFilterEnabled("module") && !moduleId && (
|
||||
{isFilterEnabled("module") && !moduleId && !moduleViewDisabled && (
|
||||
<div className="py-2">
|
||||
<FilterModule
|
||||
appliedFilters={filters.module ?? null}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-grow items-center gap-3">
|
||||
{displayProperties && displayProperties?.key && (
|
||||
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
|
||||
|
||||
@@ -24,7 +24,7 @@ import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState } from "@/hooks/store";
|
||||
import { useEventTracker, useEstimate, useLabel, useIssues, useProjectState, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// components
|
||||
import { IssuePropertyLabels } from "../properties/labels";
|
||||
@@ -45,6 +45,7 @@ export interface IIssueProperties {
|
||||
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const { issue, updateIssue, displayProperties, activeLayout, isReadOnly, className } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { labelMap } = useLabel();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const {
|
||||
@@ -56,6 +57,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
const { getStateById } = useProjectState();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@@ -349,36 +351,40 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
{/* modules */}
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
|
||||
<div className="h-5">
|
||||
<ModuleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleModule}
|
||||
disabled={isReadOnly}
|
||||
multiple
|
||||
buttonVariant="border-with-text"
|
||||
showCount
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
{projectDetails?.module_view && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
|
||||
<div className="h-5">
|
||||
<ModuleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleModule}
|
||||
disabled={isReadOnly}
|
||||
multiple
|
||||
buttonVariant="border-with-text"
|
||||
showCount
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
|
||||
{/* cycles */}
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
|
||||
<div className="h-5">
|
||||
<CycleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.cycle_id}
|
||||
onChange={handleCycle}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
{projectDetails?.cycle_view && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
|
||||
<div className="h-5">
|
||||
<CycleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.cycle_id}
|
||||
onChange={handleCycle}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{areEstimatesEnabledForCurrentProject && (
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
// icons
|
||||
import { ArchiveRestoreIcon, ExternalLink, Link, Trash2 } from "lucide-react";
|
||||
// hooks
|
||||
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
|
||||
import { DeleteIssueModal } from "@/components/issues";
|
||||
// ui
|
||||
import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { DeleteIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
import { useEventTracker, useIssues, useUser } from "@/hooks/store";
|
||||
// components
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUser } from "@/hooks/store";
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props;
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
@@ -46,6 +47,24 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
const handleIssueRestore = async () => {
|
||||
if (!handleRestore) return;
|
||||
await handleRestore()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Restore success",
|
||||
message: "Your issue can be found in project issues.",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be restored. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,7 +84,7 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
ellipsis
|
||||
>
|
||||
{isRestoringAllowed && (
|
||||
<CustomMenu.MenuItem onClick={handleRestore}>
|
||||
<CustomMenu.MenuItem onClick={handleIssueRestore}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveRestoreIcon className="h-3 w-3" />
|
||||
Restore
|
||||
@@ -100,4 +119,4 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -327,32 +327,38 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
</h3>
|
||||
</div>
|
||||
{watch("parent_id") && selectedParentIssue && (
|
||||
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: selectedParentIssue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-custom-text-200">
|
||||
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||
</span>
|
||||
<span className="truncate font-medium">{selectedParentIssue.name.substring(0, 50)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
setValue("parent_id", null);
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(null);
|
||||
}}
|
||||
tabIndex={getTabIndex("remove_parent")}
|
||||
>
|
||||
<X className="h-3 w-3 cursor-pointer" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
render={({ field: { onChange } }) => (
|
||||
<div className="flex w-min items-center gap-2 whitespace-nowrap rounded bg-custom-background-80 p-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: selectedParentIssue.state__color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-custom-text-200">
|
||||
{selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id}
|
||||
</span>
|
||||
<span className="truncate font-medium">{selectedParentIssue.name.substring(0, 50)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
handleFormChange();
|
||||
setSelectedParentIssue(null);
|
||||
}}
|
||||
tabIndex={getTabIndex("remove_parent")}
|
||||
>
|
||||
<X className="h-3 w-3 cursor-pointer" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<div className="mt-2 space-y-3">
|
||||
@@ -535,7 +541,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
handleFormChange();
|
||||
}}
|
||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "border-with-text"}
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
tabIndex={getTabIndex("assignee_ids")}
|
||||
@@ -657,33 +663,23 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1 text-xs text-custom-text-200 hover:bg-custom-background-80"
|
||||
>
|
||||
{watch("parent_id") ? (
|
||||
<div className="flex items-center gap-1 text-custom-text-200">
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue &&
|
||||
`${selectedParentIssue.project__identifier}-
|
||||
${selectedParentIssue.sequence_id}`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-custom-text-300">
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
tabIndex={getTabIndex("parent_id")}
|
||||
>
|
||||
{watch("parent_id") ? (
|
||||
{watch("parent_id") ? (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">
|
||||
{selectedParentIssue &&
|
||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
placement="bottom-start"
|
||||
tabIndex={getTabIndex("parent_id")}
|
||||
>
|
||||
<>
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
|
||||
Change parent issue
|
||||
@@ -698,12 +694,17 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
Remove parent issue
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>
|
||||
Select parent Issue
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</CustomMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2 py-1.5 text-xs hover:bg-custom-background-80"
|
||||
onClick={() => setParentIssueListModalOpen(true)}
|
||||
>
|
||||
<LayoutPanelTop className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="whitespace-nowrap">Add parent</span>
|
||||
</button>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="parent_id"
|
||||
|
||||
@@ -132,6 +132,8 @@ export const IssuesMobileHeader = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
@@ -154,6 +156,8 @@ export const IssuesMobileHeader = observer(() => {
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +87,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
projectId={issue.project_id}
|
||||
issueId={issueId}
|
||||
currentUser={currentUser}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -135,22 +135,12 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issue archived successfully.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Issue could not be archived. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||
@@ -163,8 +153,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
await restoreIssue(workspaceSlug, projectId, issueId);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Issue restored successfully.",
|
||||
title: "Restore success",
|
||||
message: "Your issue can be found in project issues.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_RESTORED,
|
||||
|
||||
@@ -177,7 +177,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="vertical-scrollbar flex h-full w-full overflow-auto">
|
||||
@@ -210,7 +215,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
<IssueActivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ArchiveModuleModal: React.FC<Props> = (props) => {
|
||||
message: "Your archives can be found in project archives.",
|
||||
});
|
||||
onClose();
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules?peekModule=${moduleId}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/modules`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -11,12 +11,13 @@ import { ProjectAnalyticsModal } from "@/components/analytics";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
// hooks
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT, ISSUE_LAYOUTS } from "@/constants/issue";
|
||||
import { useIssues, useLabel, useMember, useModule, useProjectState } from "@/hooks/store";
|
||||
import { useIssues, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
|
||||
// types
|
||||
// constants
|
||||
|
||||
export const ModuleMobileHeader = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getModuleById } = useModule();
|
||||
const layouts = [
|
||||
{ key: "list", title: "List", icon: List },
|
||||
@@ -134,6 +135,8 @@ export const ModuleMobileHeader = observer(() => {
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
@@ -157,6 +160,8 @@ export const ModuleMobileHeader = observer(() => {
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
handleDisplayPropertiesUpdate={handleDisplayProperties}
|
||||
ignoreGroupedFilters={["module"]}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ModuleQuickActions: React.FC<Props> = observer((props) => {
|
||||
title: "Restore success",
|
||||
message: "Your module can be found in project modules.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/modules/${moduleId}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -227,7 +227,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
title: "Restore success",
|
||||
message: "Your module can be found in project modules.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/modules/${moduleId}`);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archives/modules`);
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
|
||||
@@ -5,6 +5,8 @@ import { DateFilterModal } from "@/components/core";
|
||||
import { FilterHeader, FilterOption } from "@/components/issues";
|
||||
// constants
|
||||
import { DATE_BEFORE_FILTER_OPTIONS } from "@/constants/filters";
|
||||
// helpers
|
||||
import { isDate } from "@/helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: string[] | null;
|
||||
@@ -24,6 +26,17 @@ export const FilterCreatedDate: React.FC<Props> = observer((props) => {
|
||||
d.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const isCustomDateSelected = () => {
|
||||
const isValidDateSelected = appliedFilters?.filter((f) => isDate(f.split(";")[0])) || [];
|
||||
return isValidDateSelected.length > 0 ? true : false;
|
||||
};
|
||||
const handleCustomDate = () => {
|
||||
if (isCustomDateSelected()) {
|
||||
const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || [];
|
||||
handleUpdate(updateAppliedFilters);
|
||||
} else setIsDateFilterModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDateFilterModalOpen && (
|
||||
@@ -52,7 +65,7 @@ export const FilterCreatedDate: React.FC<Props> = observer((props) => {
|
||||
multiple
|
||||
/>
|
||||
))}
|
||||
<FilterOption isChecked={false} onClick={() => setIsDateFilterModalOpen(true)} title="Custom" multiple />
|
||||
<FilterOption isChecked={isCustomDateSelected()} onClick={handleCustomDate} title="Custom" multiple />
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs italic text-custom-text-400">No matches found</p>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IProjectView, IIssueFilterOptions } from "@plane/types";
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "@/components/issues";
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
|
||||
import { useLabel, useMember, useProjectState } from "@/hooks/store";
|
||||
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
|
||||
// components
|
||||
// ui
|
||||
// types
|
||||
@@ -27,6 +27,7 @@ const defaultValues: Partial<IProjectView> = {
|
||||
export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
@@ -184,6 +185,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
labels={projectLabels ?? undefined}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
@@ -212,8 +215,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
? "Updating View..."
|
||||
: "Update View"
|
||||
: isSubmitting
|
||||
? "Creating View..."
|
||||
: "Create View"}
|
||||
? "Creating View..."
|
||||
: "Create View"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -224,3 +224,7 @@ export const getDate = (date: string | Date | undefined | null): Date | undefine
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
export const isDate = (date: string) => {
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
|
||||
return datePattern.test(date);
|
||||
};
|
||||
|
||||
@@ -66,13 +66,8 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success",
|
||||
message:
|
||||
issue &&
|
||||
`${getProjectById(issue.project_id)
|
||||
?.identifier}-${issue?.sequence_id} is restored successfully under the project ${getProjectById(
|
||||
issue.project_id
|
||||
)?.name}`,
|
||||
title: "Restore success",
|
||||
message: "Your issue can be found in project issues.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`);
|
||||
})
|
||||
@@ -80,7 +75,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
message: "Issue could not be restored. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsRestoring(false));
|
||||
|
||||
Reference in New Issue
Block a user