Merge branch 'develop' of github.com:makeplane/plane into feat/connected_accounts

This commit is contained in:
pablohashescobar 2023-12-18 18:04:25 +05:30
commit 363166f491
65 changed files with 651 additions and 258 deletions

View File

@ -1010,11 +1010,18 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
files = [] files = []
s3 = boto3.client( s3_client_params = {
"s3", "service_name": "s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID, "aws_access_key_id": settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, "aws_secret_access_key": settings.AWS_SECRET_ACCESS_KEY,
) }
# Use AWS_S3_ENDPOINT_URL if it is present in the settings
if hasattr(settings, "AWS_S3_ENDPOINT_URL") and settings.AWS_S3_ENDPOINT_URL:
s3_client_params["endpoint_url"] = settings.AWS_S3_ENDPOINT_URL
s3 = boto3.client(**s3_client_params)
params = { params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/", "Prefix": "static/project-cover/",
@ -1027,9 +1034,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
if not content["Key"].endswith( if not content["Key"].endswith(
"/" "/"
): # This line ensures we're only getting files, not "sub-folders" ): # This line ensures we're only getting files, not "sub-folders"
files.append( if (
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" hasattr(settings, "AWS_S3_CUSTOM_DOMAIN")
) and settings.AWS_S3_CUSTOM_DOMAIN
and hasattr(settings, "AWS_S3_URL_PROTOCOL")
and settings.AWS_S3_URL_PROTOCOL
):
files.append(
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/{content['Key']}"
)
else:
files.append(
f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
)
return Response(files, status=status.HTTP_200_OK) return Response(files, status=status.HTTP_200_OK)

View File

@ -30,7 +30,7 @@ openpyxl==3.1.2
beautifulsoup4==4.12.2 beautifulsoup4==4.12.2
dj-database-url==2.1.0 dj-database-url==2.1.0
posthog==3.0.2 posthog==3.0.2
cryptography==41.0.5 cryptography==41.0.6
lxml==4.9.3 lxml==4.9.3
boto3==1.28.40 boto3==1.28.40

View File

@ -39,7 +39,7 @@ function download(){
echo "" echo ""
echo "Latest version is now available for you to use" echo "Latest version is now available for you to use"
echo "" echo ""
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file." echo "In case of Upgrade, your new setting file is available as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
echo "" echo ""
} }

View File

@ -19,7 +19,7 @@ type Props = {
icon?: any; icon?: any;
text: string; text: string;
onClick: () => void; onClick: () => void;
}; } | null;
disabled?: boolean; disabled?: boolean;
}; };

View File

@ -21,6 +21,7 @@ export interface EmailFormValues {
EMAIL_HOST_PASSWORD: string; EMAIL_HOST_PASSWORD: string;
EMAIL_USE_TLS: string; EMAIL_USE_TLS: string;
// EMAIL_USE_SSL: string; // EMAIL_USE_SSL: string;
EMAIL_FROM: string;
} }
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => { export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
@ -45,6 +46,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"], EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"], EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"], // EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
}, },
}); });
@ -168,6 +170,31 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
</div> </div>
</div> </div>
</div> </div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-2">
<div className="flex flex-col gap-1">
<h4 className="text-sm">From address</h4>
<Controller
control={control}
name="EMAIL_FROM"
render={({ field: { value, onChange, ref } }) => (
<Input
id="EMAIL_FROM"
name="EMAIL_FROM"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.EMAIL_FROM)}
placeholder="no-reply@projectplane.so"
className="w-full rounded-md font-medium"
/>
)}
/>
<p className="text-xs text-custom-text-400">
You will have to verify your email address to being sending emails.
</p>
</div>
</div>
<div className="flex w-full max-w-md flex-col gap-y-8 px-1"> <div className="flex w-full max-w-md flex-col gap-y-8 px-1">
<div className="mr-8 flex items-center gap-10 pt-4"> <div className="mr-8 flex items-center gap-10 pt-4">

View File

@ -120,8 +120,8 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, EIssueActions.UPDATE) await handleIssues(issueToUpdate.target_date ?? "", issueToUpdate as IIssue, action)
} }
/> />
)} )}

View File

@ -36,13 +36,17 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
const menuActionRef = useRef<HTMLDivElement | null>(null); const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: IIssue) => { const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false)); useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
@ -75,7 +79,7 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
onClick={() => handleIssuePeekOverview(issue)} onClick={(e) => handleIssuePeekOverview(issue, e)}
> >
{issue?.tempId !== undefined && ( {issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />

View File

@ -31,14 +31,18 @@ export const ProjectEmptyState: React.FC = observer(() => {
description: description:
"Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.",
}} }}
primaryButton={{ primaryButton={
text: "Create your first issue", isEditingAllowed
icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />, ? {
onClick: () => { text: "Create your first issue",
setTrackElement("PROJECT_EMPTY_STATE"); icon: <PlusIcon className="h-3 w-3" strokeWidth={2} />,
commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT); onClick: () => {
}, setTrackElement("PROJECT_EMPTY_STATE");
}} commandPaletteStore.toggleCreateIssueModal(true, EProjectStore.PROJECT);
},
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
</div> </div>

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useCallback } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// mobx store // mobx store
@ -25,6 +25,8 @@ import {
IViewIssuesStore, IViewIssuesStore,
} from "store/issues"; } from "store/issues";
import { TUnGroupedIssues } from "store/issues/types"; import { TUnGroupedIssues } from "store/issues/types";
import { EIssueActions } from "../types";
// constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
interface IBaseGanttRoot { interface IBaseGanttRoot {
@ -35,10 +37,15 @@ interface IBaseGanttRoot {
| IViewIssuesFilterStore; | IViewIssuesFilterStore;
issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore; issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore;
viewId?: string; viewId?: string;
issueActions: {
[EIssueActions.DELETE]: (issue: IIssue) => Promise<void>;
[EIssueActions.UPDATE]?: (issue: IIssue) => Promise<void>;
[EIssueActions.REMOVE]?: (issue: IIssue) => Promise<void>;
};
} }
export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => { export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGanttRoot) => {
const { issueFiltersStore, issueStore, viewId } = props; const { issueFiltersStore, issueStore, viewId, issueActions } = props;
const router = useRouter(); const router = useRouter();
const { workspaceSlug, peekIssueId, peekProjectId } = router.query; const { workspaceSlug, peekIssueId, peekProjectId } = router.query;
@ -64,11 +71,14 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId); await issueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, payload, viewId);
}; };
const updateIssue = async (projectId: string, issueId: string, payload: Partial<IIssue>) => { const handleIssues = useCallback(
if (!workspaceSlug) return; async (issue: IIssue, action: EIssueActions) => {
if (issueActions[action]) {
await issueStore.updateIssue(workspaceSlug.toString(), projectId, issueId, payload, viewId); await issueActions[action]!(issue);
}; }
},
[issueActions]
);
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -102,8 +112,8 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => { handleIssue={async (issueToUpdate, action) => {
await updateIssue(peekProjectId.toString(), peekIssueId.toString(), issueToUpdate); await handleIssues(issueToUpdate as IIssue, action);
}} }}
/> />
)} )}

View File

@ -9,13 +9,17 @@ import { IIssue } from "types";
export const IssueGanttBlock = ({ data }: { data: IIssue }) => { export const IssueGanttBlock = ({ data }: { data: IIssue }) => {
const router = useRouter(); const router = useRouter();
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${data?.workspace_detail.slug}/projects/${data?.project_detail.id}/issues/${data?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project },
});
}
}; };
return ( return (

View File

@ -4,15 +4,43 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const CycleGanttLayout: React.FC = observer(() => { export const CycleGanttLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { cycleId } = router.query; const { cycleId, workspaceSlug } = router.query;
const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore(); const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, cycleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId) return;
await cycleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, cycleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !cycleId || !issue.bridge_id) return;
await cycleIssueStore.removeIssueFromCycle(
workspaceSlug.toString(),
issue.project,
cycleId.toString(),
issue.id,
issue.bridge_id
);
},
};
return ( return (
<BaseGanttRoot <BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={cycleIssueFilterStore} issueFiltersStore={cycleIssueFilterStore}
issueStore={cycleIssueStore} issueStore={cycleIssueStore}
viewId={cycleId?.toString()} viewId={cycleId?.toString()}

View File

@ -4,15 +4,43 @@ import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const ModuleGanttLayout: React.FC = observer(() => { export const ModuleGanttLayout: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { moduleId } = router.query; const { moduleId, workspaceSlug } = router.query;
const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore(); const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore();
const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId.toString());
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId) return;
await moduleIssueStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id, moduleId.toString());
},
[EIssueActions.REMOVE]: async (issue: IIssue) => {
if (!workspaceSlug || !moduleId || !issue.bridge_id) return;
await moduleIssueStore.removeIssueFromModule(
workspaceSlug.toString(),
issue.project,
moduleId.toString(),
issue.id,
issue.bridge_id
);
},
};
return ( return (
<BaseGanttRoot <BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={moduleIssueFilterStore} issueFiltersStore={moduleIssueFilterStore}
issueStore={moduleIssueStore} issueStore={moduleIssueStore}
viewId={moduleId?.toString()} viewId={moduleId?.toString()}

View File

@ -1,12 +1,36 @@
import React from "react"; import React from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const GanttLayout: React.FC = observer(() => { export const GanttLayout: React.FC = observer(() => {
const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore(); const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
return <BaseGanttRoot issueFiltersStore={projectIssueFiltersStore} issueStore={projectIssuesStore} />; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssuesStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueFiltersStore}
issueStore={projectIssuesStore}
/>
);
}); });

View File

@ -1,11 +1,35 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// components // components
import { BaseGanttRoot } from "./base-gantt-root"; import { BaseGanttRoot } from "./base-gantt-root";
// types
import { EIssueActions } from "../types";
import { IIssue } from "types";
export const ProjectViewGanttLayout: React.FC = observer(() => { export const ProjectViewGanttLayout: React.FC = observer(() => {
const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore(); const { viewIssues: projectIssueViewStore, viewIssuesFilter: projectIssueViewFiltersStore } = useMobxStore();
const router = useRouter();
const { workspaceSlug } = router.query;
return <BaseGanttRoot issueFiltersStore={projectIssueViewFiltersStore} issueStore={projectIssueViewStore} />; const issueActions = {
[EIssueActions.UPDATE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue);
},
[EIssueActions.DELETE]: async (issue: IIssue) => {
if (!workspaceSlug) return;
await projectIssueViewStore.removeIssue(workspaceSlug.toString(), issue.project, issue.id);
},
};
return (
<BaseGanttRoot
issueActions={issueActions}
issueFiltersStore={projectIssueViewFiltersStore}
issueStore={projectIssueViewStore}
/>
);
}); });

View File

@ -346,8 +346,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, EIssueActions.UPDATE) await handleIssues(sub_group_by, group_by, issueToUpdate as IIssue, action)
} }
/> />
)} )}

View File

@ -1,5 +1,5 @@
import { memo } from "react"; import { memo } from "react";
import { Draggable } from "@hello-pangea/dnd"; import { Draggable, DraggableStateSnapshot } from "@hello-pangea/dnd";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
// components // components
import { KanBanProperties } from "./properties"; import { KanBanProperties } from "./properties";
@ -32,11 +32,23 @@ interface IssueDetailsBlockProps {
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | null; displayProperties: IIssueDisplayProperties | null;
isReadOnly: boolean; isReadOnly: boolean;
snapshot: DraggableStateSnapshot;
isDragDisabled: boolean;
} }
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => { const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
const { sub_group_id, columnId, issue, showEmptyGroup, handleIssues, quickActions, displayProperties, isReadOnly } = const {
props; sub_group_id,
columnId,
issue,
showEmptyGroup,
handleIssues,
quickActions,
displayProperties,
isReadOnly,
snapshot,
isDragDisabled,
} = props;
const router = useRouter(); const router = useRouter();
@ -44,20 +56,29 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE); if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
return ( return (
<> <div
className={`flex flex-col space-y-2 cursor-pointer rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all w-full ${
isDragDisabled ? "" : "hover:cursor-grab"
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`}
onClick={handleIssuePeekOverview}
>
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="relative"> <div className="relative w-full ">
<div className="line-clamp-1 text-xs text-custom-text-300"> <div className="line-clamp-1 text-xs text-left text-custom-text-300">
{issue.project_detail.identifier}-{issue.sequence_id} {issue.project_detail.identifier}-{issue.sequence_id}
</div> </div>
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block"> <div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">
@ -70,9 +91,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
</div> </div>
)} )}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}> <div className="line-clamp-2 text-sm font-medium text-custom-text-100">{issue.name}</div>
{issue.name}
</div>
</Tooltip> </Tooltip>
<div> <div>
<KanBanProperties <KanBanProperties
@ -85,7 +104,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = (props) => {
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
/> />
</div> </div>
</> </div>
); );
}; };
@ -132,22 +151,18 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
{issue.tempId !== undefined && ( {issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)} )}
<div <KanbanIssueMemoBlock
className={`space-y-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 px-3 py-2 text-sm shadow-custom-shadow-2xs transition-all ${ sub_group_id={sub_group_id}
isDragDisabled ? "" : "hover:cursor-grab" columnId={columnId}
} ${snapshot.isDragging ? `border-custom-primary-100` : `border-transparent`}`} issue={issue}
> showEmptyGroup={showEmptyGroup}
<KanbanIssueMemoBlock handleIssues={handleIssues}
sub_group_id={sub_group_id} quickActions={quickActions}
columnId={columnId} displayProperties={displayProperties}
issue={issue} isReadOnly={!canEditIssueProperties}
showEmptyGroup={showEmptyGroup} snapshot={snapshot}
handleIssues={handleIssues} isDragDisabled={isDragDisabled}
quickActions={quickActions} />
displayProperties={displayProperties}
isReadOnly={!canEditIssueProperties}
/>
</div>
</div> </div>
)} )}
</Draggable> </Draggable>

View File

@ -57,7 +57,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
); );
}; };
const handleStartDate = (date: string) => { const handleStartDate = (date: string | null) => {
handleIssues( handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id, !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id, !group_id && group_id === "null" ? null : group_id,
@ -65,7 +65,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
); );
}; };
const handleTargetDate = (date: string) => { const handleTargetDate = (date: string | null) => {
handleIssues( handleIssues(
!sub_group_id && sub_group_id === "null" ? null : sub_group_id, !sub_group_id && sub_group_id === "null" ? null : sub_group_id,
!group_id && group_id === "null" ? null : group_id, !group_id && group_id === "null" ? null : group_id,
@ -122,7 +122,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{displayProperties && displayProperties?.start_date && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date) => handleStartDate(date)}
disabled={isReadOnly} disabled={isReadOnly}
type="start_date" type="start_date"
/> />
@ -132,7 +132,7 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{displayProperties && displayProperties?.due_date && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} onChange={(date) => handleTargetDate(date)}
disabled={isReadOnly} disabled={isReadOnly}
type="target_date" type="target_date"
/> />

View File

@ -168,7 +168,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE)} handleIssue={async (issueToUpdate, action: EIssueActions) =>
await handleIssues(issueToUpdate as IIssue, action)
}
/> />
)} )}
</> </>

View File

@ -25,20 +25,27 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
handleIssues(issueToUpdate, EIssueActions.UPDATE); handleIssues(issueToUpdate, EIssueActions.UPDATE);
}; };
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
const canEditIssueProperties = canEditProperties(issue.project); const canEditIssueProperties = canEditProperties(issue.project);
return ( return (
<> <>
<div className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm"> <button
className="relative flex items-center gap-3 bg-custom-background-100 p-3 text-sm w-full"
onClick={handleIssuePeekOverview}
>
{displayProperties && displayProperties?.key && ( {displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-custom-text-300"> <div className="flex-shrink-0 text-xs font-medium text-custom-text-300">
{issue?.project_detail?.identifier}-{issue.sequence_id} {issue?.project_detail?.identifier}-{issue.sequence_id}
@ -49,10 +56,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" /> <div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
)} )}
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div <div className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100 text-left">
className="line-clamp-1 w-full cursor-pointer text-sm font-medium text-custom-text-100"
onClick={handleIssuePeekOverview}
>
{issue.name} {issue.name}
</div> </div>
</Tooltip> </Tooltip>
@ -75,7 +79,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
</div> </div>
)} )}
</div> </div>
</div> </button>
</> </>
); );
}; };

View File

@ -40,11 +40,11 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids });
}; };
const handleStartDate = (date: string) => { const handleStartDate = (date: string | null) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date });
}; };
const handleTargetDate = (date: string) => { const handleTargetDate = (date: string | null) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date });
}; };
@ -106,7 +106,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
{displayProperties && displayProperties?.start_date && ( {displayProperties && displayProperties?.start_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.start_date || null} value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)} onChange={(date) => handleStartDate(date)}
disabled={isReadonly} disabled={isReadonly}
type="start_date" type="start_date"
/> />
@ -116,7 +116,7 @@ export const ListProperties: FC<IListProperties> = observer((props) => {
{displayProperties && displayProperties?.due_date && ( {displayProperties && displayProperties?.due_date && (
<IssuePropertyDate <IssuePropertyDate
value={issue?.target_date || null} value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} onChange={(date) => handleTargetDate(date)}
disabled={isReadonly} disabled={isReadonly}
type="target_date" type="target_date"
/> />

View File

@ -142,7 +142,10 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
className={`flex w-full items-center justify-between gap-1 text-xs ${ className={`flex w-full items-center justify-between gap-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => (!projectId || !_members[projectId]) && getProjectMembers()} onClick={(e) => {
e.stopPropagation();
(!projectId || !_members[projectId]) && getProjectMembers();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -178,6 +181,7 @@ export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer(
active && !selected ? "bg-custom-background-80" : "" active && !selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -12,11 +12,11 @@ import { Tooltip } from "@plane/ui";
// hooks // hooks
import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown";
// helpers // helpers
import { renderDateFormat } from "helpers/date-time.helper"; import { renderDateFormat, renderFormattedDate } from "helpers/date-time.helper";
export interface IIssuePropertyDate { export interface IIssuePropertyDate {
value: any; value: string | null;
onChange: (date: any) => void; onChange: (date: string | null) => void;
disabled?: boolean; disabled?: boolean;
type: "start_date" | "target_date"; type: "start_date" | "target_date";
} }
@ -56,32 +56,41 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
return ( return (
<> <>
<Popover.Button <Popover.Button
as="button"
type="button"
ref={dropdownBtn} ref={dropdownBtn}
className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${ className="border-none outline-none"
disabled onClick={(e) => e.stopPropagation()}
? "pointer-events-none cursor-not-allowed text-custom-text-200"
: "cursor-pointer hover:bg-custom-background-80"
}`}
> >
<div className="flex items-center justify-center gap-2 overflow-hidden"> <Tooltip
<dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} /> tooltipHeading={dateOptionDetails.placeholder}
{value && ( tooltipContent={value ? renderFormattedDate(value) : "None"}
<> >
<Tooltip tooltipHeading={dateOptionDetails.placeholder} tooltipContent={value ?? "None"}> <div
<div className="text-xs">{value}</div> className={`flex h-5 w-full items-center rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 outline-none duration-300 ${
</Tooltip> disabled
? "pointer-events-none cursor-not-allowed text-custom-text-200"
<div : "cursor-pointer hover:bg-custom-background-80"
className="flex flex-shrink-0 items-center justify-center" }`}
onClick={() => { >
if (onChange) onChange(null); <div className="flex items-center justify-center gap-2 overflow-hidden">
}} <dateOptionDetails.icon className="h-3 w-3" strokeWidth={2} />
> {value && (
<X className="h-2.5 w-2.5" strokeWidth={2} /> <>
</div> <div className="text-xs">{value}</div>
</> <div
)} className="flex flex-shrink-0 items-center justify-center"
</div> onClick={() => {
if (onChange) onChange(null);
}}
>
<X className="h-2.5 w-2.5" strokeWidth={2} />
</div>
</>
)}
</div>
</div>
</Tooltip>
</Popover.Button> </Popover.Button>
<div className={`${open ? "fixed left-0 top-0 z-20 h-full w-full cursor-auto" : ""}`}> <div className={`${open ? "fixed left-0 top-0 z-20 h-full w-full cursor-auto" : ""}`}>
@ -92,7 +101,8 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
{({ close }) => ( {({ close }) => (
<DatePicker <DatePicker
selected={value ? new Date(value) : new Date()} selected={value ? new Date(value) : new Date()}
onChange={(val: any) => { onChange={(val, e) => {
e?.stopPropagation();
if (onChange && val) { if (onChange && val) {
onChange(renderDateFormat(val)); onChange(renderDateFormat(val));
close(); close();

View File

@ -116,6 +116,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${ className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={(e) => e.stopPropagation()}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -150,6 +151,7 @@ export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observe
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -107,7 +107,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
{projectLabels {projectLabels
?.filter((l) => value.includes(l.id)) ?.filter((l) => value.includes(l.id))
.map((label) => ( .map((label) => (
<Tooltip position="top" tooltipHeading="Labels" tooltipContent={label.name ?? ""}> <Tooltip position="top" tooltipHeading="Label" tooltipContent={label.name ?? ""}>
<div <div
key={label.id} key={label.id}
className={`flex overflow-hidden hover:bg-custom-background-80 ${ className={`flex overflow-hidden hover:bg-custom-background-80 ${
@ -145,14 +145,16 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
</div> </div>
) )
) : ( ) : (
<div <Tooltip position="top" tooltipHeading="Labels" tooltipContent="None">
className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${ <div
noLabelBorder ? "" : "border-[0.5px] border-custom-border-300" className={`h-full flex items-center justify-center gap-2 rounded px-2.5 py-1 text-xs hover:bg-custom-background-80 ${
}`} noLabelBorder ? "" : "border-[0.5px] border-custom-border-300"
> }`}
<Tags className="h-3.5 w-3.5" strokeWidth={2} /> >
{placeholderText} <Tags className="h-3.5 w-3.5" strokeWidth={2} />
</div> {placeholderText}
</div>
</Tooltip>
)} )}
</div> </div>
); );
@ -177,7 +179,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer" ? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !storeLabels && fetchLabels()} onClick={(e) => {
e.stopPropagation();
!storeLabels && fetchLabels();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -214,6 +219,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
selected ? "text-custom-text-100" : "text-custom-text-200" selected ? "text-custom-text-100" : "text-custom-text-200"
}` }`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -121,7 +121,10 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${ className={`flex h-5 w-full items-center justify-between gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !storeStates && fetchProjectStates()} onClick={(e) => {
e.stopPropagation();
!storeStates && fetchProjectStates();
}}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@ -157,6 +160,7 @@ export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -58,7 +58,12 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.PROJECT} currentStore={EProjectStore.PROJECT}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -40,7 +40,12 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
handleClose={() => setDeleteIssueModal(false)} handleClose={() => setDeleteIssueModal(false)}
onSubmit={handleDelete} onSubmit={handleDelete}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -58,7 +58,12 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.CYCLE} currentStore={EProjectStore.CYCLE}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -58,7 +58,13 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
}} }}
currentStore={EProjectStore.MODULE} currentStore={EProjectStore.MODULE}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis>
<CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -68,7 +68,12 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
}} }}
currentStore={EProjectStore.PROJECT} currentStore={EProjectStore.PROJECT}
/> />
<CustomMenu placement="bottom-start" customButton={customActionButton} ellipsis> <CustomMenu
placement="bottom-start"
customButton={customActionButton}
ellipsis
menuButtonOnClick={(e) => e.stopPropagation()}
>
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View File

@ -34,13 +34,17 @@ export const IssueColumn: React.FC<Props> = ({
const menuActionRef = useRef<HTMLDivElement | null>(null); const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: IIssue) => { const handleIssuePeekOverview = (issue: IIssue, event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
const paddingLeft = `${nestingLevel * 54}px`; const paddingLeft = `${nestingLevel * 54}px`;
@ -99,7 +103,7 @@ export const IssueColumn: React.FC<Props> = ({
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}> <Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<div <div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100" className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
onClick={() => handleIssuePeekOverview(issue)} onClick={(e) => handleIssuePeekOverview(issue, e)}
> >
{issue.name} {issue.name}
</div> </div>

View File

@ -6,8 +6,6 @@ import { IssuePropertyState } from "../../properties";
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // types
import { IIssue, IState } from "types"; import { IIssue, IState } from "types";
import { mutate } from "swr";
import { SUB_ISSUES } from "constants/fetch-keys";
type Props = { type Props = {
issue: IIssue; issue: IIssue;

View File

@ -194,7 +194,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate: any) => await handleIssues(issueToUpdate, EIssueActions.UPDATE)} handleIssue={async (issueToUpdate: any, action: EIssueActions) => await handleIssues(issueToUpdate, action)}
/> />
)} )}
</div> </div>

View File

@ -47,7 +47,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
} = useMobxStore(); } = useMobxStore();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, peekProjectId: projectId } = router.query;
const handleState = (_state: string) => { const handleState = (_state: string) => {
issueUpdate({ ...issue, state: _state }); issueUpdate({ ...issue, state: _state });
@ -116,7 +116,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<p>State</p> <p>State</p>
</div> </div>
<div> <div>
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={disableUserActions} /> <SidebarStateSelect
value={issue?.state || ""}
projectId={projectId as string}
onChange={handleState}
disabled={disableUserActions}
/>
</div> </div>
</div> </div>
@ -129,6 +134,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarAssigneeSelect <SidebarAssigneeSelect
value={issue.assignees || []} value={issue.assignees || []}
projectId={projectId as string}
onChange={handleAssignee} onChange={handleAssignee}
disabled={disableUserActions} disabled={disableUserActions}
/> />
@ -210,7 +216,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<p>Parent</p> <p>Parent</p>
</div> </div>
<div> <div>
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={disableUserActions} /> <SidebarParentSelect
onChange={handleParent}
issueDetails={issue}
projectId={projectId as string}
disabled={disableUserActions}
/>
</div> </div>
</div> </div>
</div> </div>
@ -226,6 +237,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarCycleSelect <SidebarCycleSelect
issueDetail={issue} issueDetail={issue}
projectId={projectId as string}
disabled={disableUserActions} disabled={disableUserActions}
handleIssueUpdate={handleCycleOrModuleChange} handleIssueUpdate={handleCycleOrModuleChange}
/> />
@ -240,6 +252,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div> <div>
<SidebarModuleSelect <SidebarModuleSelect
issueDetail={issue} issueDetail={issue}
projectId={projectId as string}
disabled={disableUserActions} disabled={disableUserActions}
handleIssueUpdate={handleCycleOrModuleChange} handleIssueUpdate={handleCycleOrModuleChange}
/> />
@ -253,6 +266,7 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
<div className="flex w-full flex-col gap-3"> <div className="flex w-full flex-col gap-3">
<SidebarLabelSelect <SidebarLabelSelect
issueDetails={issue} issueDetails={issue}
projectId={projectId as string}
labelList={issue.labels} labelList={issue.labels}
submitChanges={handleLabels} submitChanges={handleLabels}
isNotAllowed={disableUserActions} isNotAllowed={disableUserActions}

View File

@ -11,6 +11,7 @@ import { IssueView } from "components/issues";
import { copyUrlToClipboard } from "helpers/string.helper"; import { copyUrlToClipboard } from "helpers/string.helper";
// types // types
import { IIssue, IIssueLink } from "types"; import { IIssue, IIssueLink } from "types";
import { EIssueActions } from "../issue-layouts/types";
// constants // constants
import { EUserWorkspaceRoles } from "constants/workspace"; import { EUserWorkspaceRoles } from "constants/workspace";
@ -18,7 +19,7 @@ interface IIssuePeekOverview {
workspaceSlug: string; workspaceSlug: string;
projectId: string; projectId: string;
issueId: string; issueId: string;
handleIssue: (issue: Partial<IIssue>) => void; handleIssue: (issue: Partial<IIssue>, action: EIssueActions) => Promise<void>;
isArchived?: boolean; isArchived?: boolean;
children?: ReactNode; children?: ReactNode;
} }
@ -30,8 +31,6 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { peekIssueId } = router.query; const { peekIssueId } = router.query;
const { const {
user: { currentProjectRole },
issue: { removeIssueFromStructure },
issueDetail: { issueDetail: {
createIssueComment, createIssueComment,
updateIssueComment, updateIssueComment,
@ -58,6 +57,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
}, },
archivedIssues: { deleteArchivedIssue }, archivedIssues: { deleteArchivedIssue },
project: { currentProjectDetails }, project: { currentProjectDetails },
workspaceMember: { currentWorkspaceUserProjectsRole },
} = useMobxStore(); } = useMobxStore();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -98,7 +98,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issueUpdate = async (_data: Partial<IIssue>) => { const issueUpdate = async (_data: Partial<IIssue>) => {
if (handleIssue) { if (handleIssue) {
await handleIssue(_data); await handleIssue(_data, EIssueActions.UPDATE);
fetchIssueActivity(workspaceSlug, projectId, issueId); fetchIssueActivity(workspaceSlug, projectId, issueId);
} }
}; };
@ -133,7 +133,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const handleDeleteIssue = async () => { const handleDeleteIssue = async () => {
if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!); if (isArchived) await deleteArchivedIssue(workspaceSlug, projectId, issue!);
else removeIssueFromStructure(workspaceSlug, projectId, issue!); else await handleIssue(issue!, EIssueActions.DELETE);
const { query } = router; const { query } = router;
if (query.peekIssueId) { if (query.peekIssueId) {
setPeekId(null); setPeekId(null);
@ -146,7 +146,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
} }
}; };
const userRole = currentProjectRole ?? EUserWorkspaceRoles.GUEST; const userRole =
(currentWorkspaceUserProjectsRole && currentWorkspaceUserProjectsRole[projectId]) ?? EUserWorkspaceRoles.GUEST;
return ( return (
<Fragment> <Fragment>

View File

@ -1,4 +1,4 @@
import { FC, ReactNode, useState } from "react"; import { FC, ReactNode, useRef, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
@ -14,6 +14,8 @@ import {
PeekOverviewIssueDetails, PeekOverviewIssueDetails,
PeekOverviewProperties, PeekOverviewProperties,
} from "components/issues"; } from "components/issues";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// ui // ui
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui"; import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
// types // types
@ -107,6 +109,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek"); const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const [deleteIssueModal, setDeleteIssueModal] = useState(false); const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
// ref
const issuePeekOverviewRef = useRef<HTMLDivElement>(null);
const updateRoutePeekId = () => { const updateRoutePeekId = () => {
if (issueId != peekIssueId) { if (issueId != peekIssueId) {
@ -151,6 +155,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
useOutsideClickDetector(issuePeekOverviewRef, () => removeRoutePeekId());
return ( return (
<> <>
{issue && !isArchived && ( {issue && !isArchived && (
@ -178,6 +184,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
{issueId === peekIssueId && ( {issueId === peekIssueId && (
<div <div
ref={issuePeekOverviewRef}
className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300 className={`fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300
${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``} ${peekMode === "side-peek" ? `bottom-0 right-0 top-0 w-full md:w-[50%]` : ``}
${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``} ${peekMode === "modal" ? `left-[50%] top-[50%] h-5/6 w-5/6 -translate-x-[50%] -translate-y-[50%]` : ``}

View File

@ -34,7 +34,7 @@ export const IssueCycleSelect: React.FC<IssueCycleSelectProps> = observer((props
if (workspaceSlug && projectId) cycleStore.fetchCycles(workspaceSlug, projectId, "all"); if (workspaceSlug && projectId) cycleStore.fetchCycles(workspaceSlug, projectId, "all");
}; };
const cycles = cycleStore.projectCycles; const cycles = cycleStore.cycles?.[projectId]?.["all"] ?? [];
const selectedCycle = cycles ? cycles?.find((i) => i.id === value) : undefined; const selectedCycle = cycles ? cycles?.find((i) => i.id === value) : undefined;

View File

@ -10,6 +10,7 @@ import { PROJECT_MEMBERS } from "constants/fetch-keys";
type Props = { type Props = {
value: string[]; value: string[];
projectId: string;
onChange: (val: string[]) => void; onChange: (val: string[]) => void;
disabled?: boolean; disabled?: boolean;
}; };
@ -17,9 +18,9 @@ type Props = {
// services // services
const projectMemberService = new ProjectMemberService(); const projectMemberService = new ProjectMemberService();
export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => { export const SidebarAssigneeSelect: React.FC<Props> = ({ value, projectId, onChange, disabled = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
const { data: members } = useSWR( const { data: members } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null, workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,

View File

@ -14,6 +14,7 @@ import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/f
type Props = { type Props = {
issueDetail: IIssue | undefined; issueDetail: IIssue | undefined;
projectId: string;
handleCycleChange?: (cycleId: string) => void; handleCycleChange?: (cycleId: string) => void;
disabled?: boolean; disabled?: boolean;
handleIssueUpdate?: () => void; handleIssueUpdate?: () => void;
@ -26,7 +27,7 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
const { issueDetail, disabled = false, handleIssueUpdate, handleCycleChange } = props; const { issueDetail, disabled = false, handleIssueUpdate, handleCycleChange } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId: _projectId, peekProjectId } = router.query;
// mobx store // mobx store
const { const {
cycleIssues: { removeIssueFromCycle, addIssueToCycle }, cycleIssues: { removeIssueFromCycle, addIssueToCycle },
@ -34,6 +35,8 @@ export const SidebarCycleSelect: React.FC<Props> = (props) => {
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const projectId = _projectId ?? peekProjectId;
const { data: incompleteCycles } = useSWR( const { data: incompleteCycles } = useSWR(
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null, workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
workspaceSlug && projectId workspaceSlug && projectId

View File

@ -18,6 +18,7 @@ import { IIssue, IIssueLabel } from "types";
type Props = { type Props = {
issueDetails: IIssue | undefined; issueDetails: IIssue | undefined;
projectId: string;
labelList: string[]; labelList: string[];
submitChanges: (formData: any) => void; submitChanges: (formData: any) => void;
isNotAllowed: boolean; isNotAllowed: boolean;
@ -30,12 +31,12 @@ const defaultValues: Partial<IIssueLabel> = {
}; };
export const SidebarLabelSelect: React.FC<Props> = observer((props) => { export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
const { issueDetails, labelList, submitChanges, isNotAllowed, uneditable } = props; const { issueDetails, projectId, labelList, submitChanges, isNotAllowed, uneditable } = props;
// states // states
const [createLabelForm, setCreateLabelForm] = useState(false); const [createLabelForm, setCreateLabelForm] = useState(false);
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
// toast // toast
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// mobx store // mobx store

View File

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { mutate } from "swr"; import useSWR, { mutate } from "swr";
// mobx store // mobx store
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
// ui // ui
@ -9,28 +9,40 @@ import { CustomSearchSelect, DiceIcon, Spinner, Tooltip } from "@plane/ui";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// fetch-keys // fetch-keys
import { ISSUE_DETAILS, MODULE_ISSUES } from "constants/fetch-keys"; import { ISSUE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
// services
import { ModuleService } from "services/module.service";
type Props = { type Props = {
issueDetail: IIssue | undefined; issueDetail: IIssue | undefined;
projectId: string;
handleModuleChange?: (moduleId: string) => void; handleModuleChange?: (moduleId: string) => void;
disabled?: boolean; disabled?: boolean;
handleIssueUpdate?: () => void; handleIssueUpdate?: () => void;
}; };
// services
const moduleService = new ModuleService();
export const SidebarModuleSelect: React.FC<Props> = observer((props) => { export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
const { issueDetail, disabled = false, handleIssueUpdate, handleModuleChange } = props; const { issueDetail, projectId, disabled = false, handleIssueUpdate, handleModuleChange } = props;
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug } = router.query;
// mobx store // mobx store
const { const {
module: { projectModules },
moduleIssues: { removeIssueFromModule, addIssueToModule }, moduleIssues: { removeIssueFromModule, addIssueToModule },
} = useMobxStore(); } = useMobxStore();
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const { data: projectModules } = useSWR(
workspaceSlug && projectId ? MODULE_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => moduleService.getModules(workspaceSlug as string, projectId as string)
: null
);
const handleModuleStoreChange = async (moduleId: string) => { const handleModuleStoreChange = async (moduleId: string) => {
if (!workspaceSlug || !issueDetail || !moduleId) return; if (!workspaceSlug || !issueDetail || !moduleId) return;

View File

@ -12,15 +12,16 @@ import { IIssue, ISearchIssueResponse } from "types";
type Props = { type Props = {
onChange: (value: string) => void; onChange: (value: string) => void;
issueDetails: IIssue | undefined; issueDetails: IIssue | undefined;
projectId: string;
disabled?: boolean; disabled?: boolean;
}; };
export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, disabled = false }) => { export const SidebarParentSelect: React.FC<Props> = ({ onChange, issueDetails, projectId, disabled = false }) => {
const [isParentModalOpen, setIsParentModalOpen] = useState(false); const [isParentModalOpen, setIsParentModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null); const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
const router = useRouter(); const router = useRouter();
const { projectId, issueId } = router.query; const { issueId } = router.query;
return ( return (
<> <>

View File

@ -15,6 +15,7 @@ import { STATES_LIST } from "constants/fetch-keys";
type Props = { type Props = {
value: string; value: string;
projectId: string;
onChange: (val: string) => void; onChange: (val: string) => void;
disabled?: boolean; disabled?: boolean;
}; };
@ -22,9 +23,9 @@ type Props = {
// services // services
const stateService = new ProjectStateService(); const stateService = new ProjectStateService();
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => { export const SidebarStateSelect: React.FC<Props> = ({ value, projectId, onChange, disabled = false }) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, inboxIssueId } = router.query; const { workspaceSlug, inboxIssueId } = router.query;
const { data: states } = useSWR( const { data: states } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null, workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,

View File

@ -290,6 +290,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarStateSelect <SidebarStateSelect
value={value} value={value}
projectId={projectId as string}
onChange={(val: string) => submitChanges({ state: val })} onChange={(val: string) => submitChanges({ state: val })}
disabled={!isAllowed || uneditable} disabled={!isAllowed || uneditable}
/> />
@ -311,6 +312,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<SidebarAssigneeSelect <SidebarAssigneeSelect
value={value} value={value}
projectId={projectId as string}
onChange={(val: string[]) => submitChanges({ assignees: val })} onChange={(val: string[]) => submitChanges({ assignees: val })}
disabled={!isAllowed || uneditable} disabled={!isAllowed || uneditable}
/> />
@ -382,6 +384,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
onChange(val); onChange(val);
}} }}
issueDetails={issueDetail} issueDetails={issueDetail}
projectId={projectId as string}
disabled={!isAllowed || uneditable} disabled={!isAllowed || uneditable}
/> />
)} )}
@ -536,6 +539,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="space-y-1"> <div className="space-y-1">
<SidebarCycleSelect <SidebarCycleSelect
issueDetail={issueDetail} issueDetail={issueDetail}
projectId={projectId as string}
handleCycleChange={handleCycleChange} handleCycleChange={handleCycleChange}
disabled={!isAllowed || uneditable} disabled={!isAllowed || uneditable}
/> />
@ -551,6 +555,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="space-y-1"> <div className="space-y-1">
<SidebarModuleSelect <SidebarModuleSelect
issueDetail={issueDetail} issueDetail={issueDetail}
projectId={projectId as string}
handleModuleChange={handleModuleChange} handleModuleChange={handleModuleChange}
disabled={!isAllowed || uneditable} disabled={!isAllowed || uneditable}
/> />
@ -569,6 +574,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
<div className="space-y-1 sm:w-1/2"> <div className="space-y-1 sm:w-1/2">
<SidebarLabelSelect <SidebarLabelSelect
issueDetails={issueDetail} issueDetails={issueDetail}
projectId={projectId as string}
labelList={issueDetail?.labels ?? []} labelList={issueDetail?.labels ?? []}
submitChanges={submitChanges} submitChanges={submitChanges}
isNotAllowed={!isAllowed} isNotAllowed={!isAllowed}

View File

@ -10,6 +10,7 @@ import { CustomMenu, Tooltip } from "@plane/ui";
// types // types
import { IUser, IIssue } from "types"; import { IUser, IIssue } from "types";
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
import { EIssueActions } from "../issue-layouts/types";
export interface ISubIssues { export interface ISubIssues {
workspaceSlug: string; workspaceSlug: string;
@ -29,6 +30,7 @@ export interface ISubIssues {
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void; handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
handleDeleteIssue: (issue: IIssue) => Promise<void>;
} }
export const SubIssues: React.FC<ISubIssues> = ({ export const SubIssues: React.FC<ISubIssues> = ({
@ -45,17 +47,22 @@ export const SubIssues: React.FC<ISubIssues> = ({
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
handleUpdateIssue, handleUpdateIssue,
handleDeleteIssue,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const { peekProjectId, peekIssueId } = router.query; const { peekProjectId, peekIssueId } = router.query;
const handleIssuePeekOverview = () => { const handleIssuePeekOverview = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { query } = router; const { query } = router;
if (event.ctrlKey || event.metaKey) {
router.push({ const issueUrl = `/${issue.workspace_detail.slug}/projects/${issue.project_detail.id}/issues/${issue?.id}`;
pathname: router.pathname, window.open(issueUrl, "_blank"); // Open link in a new tab
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project }, } else {
}); router.push({
pathname: router.pathname,
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project },
});
}
}; };
return ( return (
@ -65,7 +72,13 @@ export const SubIssues: React.FC<ISubIssues> = ({
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={peekProjectId.toString()} projectId={peekProjectId.toString()}
issueId={peekIssueId.toString()} issueId={peekIssueId.toString()}
handleIssue={async (issueToUpdate) => await handleUpdateIssue(issue, { ...issue, ...issueToUpdate })} handleIssue={async (issueToUpdate, action) => {
if (action === EIssueActions.UPDATE) {
await handleUpdateIssue(issue, { ...issue, ...issueToUpdate });
} else if (action === EIssueActions.DELETE) {
await handleDeleteIssue(issue);
}
}}
/> />
)} )}
<div> <div>
@ -176,6 +189,7 @@ export const SubIssues: React.FC<ISubIssues> = ({
{issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && ( {issuesLoader.visibility.includes(issue?.id) && issue?.sub_issues_count > 0 && (
<SubIssuesRootList <SubIssuesRootList
handleDeleteIssue={handleDeleteIssue}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
parentIssue={issue} parentIssue={issue}

View File

@ -27,6 +27,7 @@ export interface ISubIssuesRootList {
issue?: IIssue | null issue?: IIssue | null
) => void; ) => void;
handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void; handleUpdateIssue: (issue: IIssue, data: Partial<IIssue>) => void;
handleDeleteIssue: (issue: IIssue) => Promise<void>
} }
const issueService = new IssueService(); const issueService = new IssueService();
@ -44,6 +45,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
copyText, copyText,
handleIssueCrudOperation, handleIssueCrudOperation,
handleUpdateIssue, handleUpdateIssue,
handleDeleteIssue
}) => { }) => {
const { data: issues, isLoading } = useSWR( const { data: issues, isLoading } = useSWR(
workspaceSlug && projectId && parentIssue && parentIssue?.id ? SUB_ISSUES(parentIssue?.id) : null, workspaceSlug && projectId && parentIssue && parentIssue?.id ? SUB_ISSUES(parentIssue?.id) : null,
@ -70,6 +72,7 @@ export const SubIssuesRootList: React.FC<ISubIssuesRootList> = ({
issues.sub_issues.length > 0 && issues.sub_issues.length > 0 &&
issues.sub_issues.map((issue: IIssue) => ( issues.sub_issues.map((issue: IIssue) => (
<SubIssues <SubIssues
handleDeleteIssue={handleDeleteIssue}
key={`${issue?.id}`} key={`${issue?.id}`}
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}

View File

@ -176,6 +176,16 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
[updateIssueStructure, projectId, updateIssue, user, workspaceSlug] [updateIssueStructure, projectId, updateIssue, user, workspaceSlug]
); );
const handleDeleteIssue = useCallback(
async (issue: IIssue) => {
if (!workspaceSlug || !projectId || !user) return;
await removeIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
await mutate(SUB_ISSUES(parentIssue?.id));
},
[removeIssue, projectId, user, workspaceSlug, parentIssue?.id]
);
const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; const isEditable = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
const mutateSubIssues = (parentIssueId: string | null) => { const mutateSubIssues = (parentIssueId: string | null) => {
@ -236,6 +246,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
{issuesLoader.visibility.includes(parentIssue?.id) && workspaceSlug && projectId && ( {issuesLoader.visibility.includes(parentIssue?.id) && workspaceSlug && projectId && (
<div className="border border-b-0 border-custom-border-100"> <div className="border border-b-0 border-custom-border-100">
<SubIssuesRootList <SubIssuesRootList
handleDeleteIssue={handleDeleteIssue}
workspaceSlug={workspaceSlug.toString()} workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()} projectId={projectId.toString()}
parentIssue={parentIssue} parentIssue={parentIssue}

View File

@ -96,11 +96,15 @@ export const ModulesListView: React.FC = observer(() => {
description: description:
"A cart module, a chassis module, and a warehouse module are all good example of this grouping.", "A cart module, a chassis module, and a warehouse module are all good example of this grouping.",
}} }}
primaryButton={{ primaryButton={
icon: <Plus className="h-4 w-4" />, isEditingAllowed
text: "Build your first module", ? {
onClick: () => commandPaletteStore.toggleCreateModuleModal(true), icon: <Plus className="h-4 w-4" />,
}} text: "Build your first module",
onClick: () => commandPaletteStore.toggleCreateModuleModal(true),
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
)} )}

View File

@ -626,13 +626,15 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
<Info className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" /> <Info className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
<span className="p-0.5 text-xs text-custom-text-300">No links added yet</span> <span className="p-0.5 text-xs text-custom-text-300">No links added yet</span>
</div> </div>
<button {isEditingAllowed && (
className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100" <button
onClick={() => setModuleLinkModal(true)} className="flex items-center gap-1.5 text-sm font-medium text-custom-primary-100"
> onClick={() => setModuleLinkModal(true)}
<Plus className="h-3 w-3" /> >
Add link <Plus className="h-3 w-3" />
</button> Add link
</button>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -93,13 +93,17 @@ export const WorkspaceDashboardView = observer(() => {
direction: "right", direction: "right",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.", description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
}} }}
primaryButton={{ primaryButton={
text: "Build your first project", isEditingAllowed
onClick: () => { ? {
setTrackElement("DASHBOARD_PAGE"); text: "Build your first project",
commandPaletteStore.toggleCreateProjectModal(true); onClick: () => {
}, setTrackElement("DASHBOARD_PAGE");
}} commandPaletteStore.toggleCreateProjectModal(true);
},
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
) )

View File

@ -58,11 +58,15 @@ export const PagesListView: FC<IPagesListView> = observer(({ pages }) => {
"We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.", "We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.",
direction: "right", direction: "right",
}} }}
primaryButton={{ primaryButton={
icon: <Plus className="h-4 w-4" />, isEditingAllowed
text: "Create your first page", ? {
onClick: () => toggleCreatePageModal(true), icon: <Plus className="h-4 w-4" />,
}} text: "Create your first page",
onClick: () => toggleCreatePageModal(true),
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
)} )}

View File

@ -66,11 +66,15 @@ export const RecentPagesList: FC = observer(() => {
"We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.", "We wrote Parth and Meeras love story. You could write your projects mission, goals, and eventual vision.",
direction: "right", direction: "right",
}} }}
primaryButton={{ primaryButton={
icon: <Plus className="h-4 w-4" />, isEditingAllowed
text: "Create your first page", ? {
onClick: () => commandPaletteStore.toggleCreatePageModal(true), icon: <Plus className="h-4 w-4" />,
}} text: "Create your first page",
onClick: () => commandPaletteStore.toggleCreatePageModal(true),
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
</> </>

View File

@ -67,13 +67,17 @@ export const ProjectCardList: FC<IProjectCardList> = observer((props) => {
direction: "right", direction: "right",
description: "A project could be a products roadmap, a marketing campaign, or launching a new car.", description: "A project could be a products roadmap, a marketing campaign, or launching a new car.",
}} }}
primaryButton={{ primaryButton={
text: "Start your first project", isEditingAllowed
onClick: () => { ? {
setTrackElement("PROJECTS_EMPTY_STATE"); text: "Start your first project",
commandPaletteStore.toggleCreateProjectModal(true); onClick: () => {
}, setTrackElement("PROJECTS_EMPTY_STATE");
}} commandPaletteStore.toggleCreateProjectModal(true);
},
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
)} )}

View File

@ -93,6 +93,7 @@ export const PrioritySelect: React.FC<Props> = ({
className={`flex h-full w-full items-center justify-between gap-1 ${ className={`flex h-full w-full items-center justify-between gap-1 ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer" disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={(e) => e.stopPropagation()}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-2.5 w-2.5" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-2.5 w-2.5" aria-hidden="true" />}
@ -127,6 +128,7 @@ export const PrioritySelect: React.FC<Props> = ({
active ? "bg-custom-background-80" : "" active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
onClick={(e) => e.stopPropagation()}
> >
{({ selected }) => ( {({ selected }) => (
<> <>

View File

@ -76,11 +76,15 @@ export const ProjectViewsList = observer(() => {
description: "You can create a view from here with as many properties as filters as you see fit.", description: "You can create a view from here with as many properties as filters as you see fit.",
direction: "right", direction: "right",
}} }}
primaryButton={{ primaryButton={
icon: <Plus size={14} strokeWidth={2} />, isEditingAllowed
text: "Build your first view", ? {
onClick: () => commandPaletteStore.toggleCreateViewModal(true), icon: <Plus size={14} strokeWidth={2} />,
}} text: "Build your first view",
onClick: () => commandPaletteStore.toggleCreateViewModal(true),
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
)} )}

View File

@ -9,7 +9,7 @@ import { CreateUpdateWorkspaceViewModal } from "components/workspace";
// icon // icon
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
// constants // constants
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; import { DEFAULT_GLOBAL_VIEWS_LIST, EUserWorkspaceRoles } from "constants/workspace";
export const GlobalViewsHeader: React.FC = observer(() => { export const GlobalViewsHeader: React.FC = observer(() => {
const [createViewModal, setCreateViewModal] = useState(false); const [createViewModal, setCreateViewModal] = useState(false);
@ -17,7 +17,10 @@ export const GlobalViewsHeader: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, globalViewId } = router.query; const { workspaceSlug, globalViewId } = router.query;
const { globalViews: globalViewsStore } = useMobxStore(); const {
globalViews: globalViewsStore,
user: { currentWorkspaceRole },
} = useMobxStore();
// bring the active view to the centre of the header // bring the active view to the centre of the header
useEffect(() => { useEffect(() => {
@ -28,11 +31,13 @@ export const GlobalViewsHeader: React.FC = observer(() => {
if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" }); if (activeTabElement) activeTabElement.scrollIntoView({ behavior: "smooth", inline: "center" });
}, [globalViewId]); }, [globalViewId]);
const isAuthorizedUser = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey); const isTabSelected = (tabKey: string) => router.pathname.includes(tabKey);
return ( return (
<> <>
<CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} /> <CreateUpdateWorkspaceViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
<div className="group relative flex w-full items-center overflow-x-scroll border-b border-custom-border-200 px-4"> <div className="group relative flex w-full items-center overflow-x-scroll border-b border-custom-border-200 px-4 py-2">
{DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => ( {DEFAULT_GLOBAL_VIEWS_LIST.map((tab) => (
<Link key={tab.key} href={`/${workspaceSlug}/workspace-views/${tab.key}`}> <Link key={tab.key} href={`/${workspaceSlug}/workspace-views/${tab.key}`}>
<span <span
@ -62,13 +67,15 @@ export const GlobalViewsHeader: React.FC = observer(() => {
</Link> </Link>
))} ))}
<button {isAuthorizedUser && (
type="button" <button
className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 py-3 hover:border-custom-border-200 hover:text-custom-text-400" type="button"
onClick={() => setCreateViewModal(true)} className="sticky -right-4 flex w-12 flex-shrink-0 items-center justify-center border-transparent bg-custom-background-100 hover:border-custom-border-200 hover:text-custom-text-400"
> onClick={() => setCreateViewModal(true)}
<Plus className="h-4 w-4 text-custom-primary-200" /> >
</button> <Plus className="h-4 w-4 text-custom-primary-200" />
</button>
)}
</div> </div>
</> </>
); );

View File

@ -103,13 +103,17 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => {
description: description:
"A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.",
}} }}
primaryButton={{ primaryButton={
icon: <Plus className="h-4 w-4" />, isEditingAllowed
text: "Set your first cycle", ? {
onClick: () => { icon: <Plus className="h-4 w-4" />,
setCreateModal(true); text: "Set your first cycle",
}, onClick: () => {
}} setCreateModal(true);
},
}
: null
}
disabled={!isEditingAllowed} disabled={!isEditingAllowed}
/> />
</div> </div>

View File

@ -82,7 +82,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
description_html: newDescription, description_html: newDescription,
}) })
.then(() => { .then(() => {
mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription }) as IPage, false); mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription } as IPage), false);
}); });
}; };
@ -162,15 +162,12 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
}, [pageDetails?.description_html]); // TODO: Verify the exhaustive-deps warning }, [pageDetails?.description_html]); // TODO: Verify the exhaustive-deps warning
function createObjectFromArray(keys: string[], options: any): any { function createObjectFromArray(keys: string[], options: any): any {
return keys.reduce( return keys.reduce((obj, key) => {
(obj, key) => { if (options[key] !== undefined) {
if (options[key] !== undefined) { obj[key] = options[key];
obj[key] = options[key]; }
} return obj;
return obj; }, {} as { [key: string]: any });
},
{} as { [key: string]: any }
);
} }
const mutatePageDetailsHelper = ( const mutatePageDetailsHelper = (
@ -499,7 +496,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
projectId={projectId as string} projectId={projectId as string}
issueId={peekIssueId ? (peekIssueId as string) : ""} issueId={peekIssueId ? (peekIssueId as string) : ""}
isArchived={false} isArchived={false}
handleIssue={(issueToUpdate) => { handleIssue={async (issueToUpdate, action) => {
if (peekIssueId && typeof peekIssueId === "string") { if (peekIssueId && typeof peekIssueId === "string") {
handleUpdateIssue(peekIssueId, issueToUpdate); handleUpdateIssue(peekIssueId, issueToUpdate);
} }

View File

@ -182,6 +182,12 @@ export class GlobalIssuesStore extends IssueBaseStore implements IGlobalIssuesSt
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[workspaceViewId][issueId] = { ..._issues[workspaceViewId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, workspaceViewId, "mutation"); this.fetchIssues(workspaceSlug, workspaceViewId, "mutation");

View File

@ -239,6 +239,12 @@ export class CycleIssuesStore extends IssueBaseStore implements ICycleIssuesStor
const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[cycleId][issueId] = { ..._issues[cycleId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);

View File

@ -158,6 +158,12 @@ export class ProjectDraftIssuesStore extends IssueBaseStore implements IProjectD
const response = await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); const response = await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[projectId][issueId] = { ..._issues[projectId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");

View File

@ -231,6 +231,12 @@ export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesSt
const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[moduleId][issueId] = { ..._issues[moduleId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId);

View File

@ -163,6 +163,12 @@ export class ViewIssuesStore extends IssueBaseStore implements IViewIssuesStore
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[projectId][issueId] = { ..._issues[projectId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");

View File

@ -163,6 +163,12 @@ export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssues
const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data);
runInAction(() => {
_issues = { ...this.issues };
_issues[projectId][issueId] = { ..._issues[projectId][issueId], ...response };
this.issues = _issues;
});
return response; return response;
} catch (error) { } catch (error) {
this.fetchIssues(workspaceSlug, projectId, "mutation"); this.fetchIssues(workspaceSlug, projectId, "mutation");

View File

@ -420,6 +420,7 @@ body {
.vertical-lr { .vertical-lr {
-webkit-writing-mode: vertical-lr; -webkit-writing-mode: vertical-lr;
-ms-writing-mode: vertical-lr; -ms-writing-mode: vertical-lr;
writing-mode: vertical-lr;
} }
div.web-view-spinner { div.web-view-spinner {