Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c9245fe1 | ||
|
|
d5cbe3283b | ||
|
|
ae931f8172 | ||
|
|
a8c6483c60 | ||
|
|
9c761a614f | ||
|
|
adf88a0f13 | ||
|
|
5d2983d027 | ||
|
|
8339daa3ee | ||
|
|
4a9e09a54a |
@@ -17,6 +17,7 @@ export const EnterKeyExtension = (onEnterKeyPress?: (descriptionHTML: string) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.newlineInCode(),
|
||||
() => commands.splitListItem("listItem"),
|
||||
() => commands.splitListItem("taskItem"),
|
||||
() => commands.createParagraphNear(),
|
||||
() => commands.liftEmptyBlock(),
|
||||
() => commands.splitBlock(),
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
return (
|
||||
<>
|
||||
<Moveable
|
||||
target={document.querySelector(".ProseMirror-selectednode") as HTMLElement}
|
||||
target={document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLElement}
|
||||
container={null}
|
||||
origin={false}
|
||||
edge={false}
|
||||
@@ -39,7 +39,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".active-editor .ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const originalWidth = Number(imageInfo.width);
|
||||
const originalHeight = Number(imageInfo.height);
|
||||
|
||||
@@ -369,7 +369,7 @@ const ProfileSettingsPage = observer(() => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{errors?.display_name && <span className="text-xs text-red-500">Please enter display name</span>}
|
||||
{errors?.display_name && <span className="text-xs text-red-500">{errors?.display_name?.message}</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -66,7 +66,6 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
onEnterKeyPress={(commentHTML) => {
|
||||
console.log("commentHTML", commentHTML);
|
||||
const isEmpty =
|
||||
commentHTML?.trim() === "" ||
|
||||
commentHTML === "<p></p>" ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useEffect, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import type { TIssueLinkEditableFields } from "@plane/types";
|
||||
@@ -27,7 +28,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
|
||||
url: "",
|
||||
};
|
||||
|
||||
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props) => {
|
||||
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = observer((props) => {
|
||||
// props
|
||||
const { isModalOpen, handleOnClose, linkOperations } = props;
|
||||
|
||||
@@ -45,6 +46,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
|
||||
const onClose = () => {
|
||||
setIssueLinkData(null);
|
||||
reset(defaultValues);
|
||||
if (handleOnClose) handleOnClose();
|
||||
};
|
||||
|
||||
@@ -55,8 +57,8 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({ ...defaultValues, ...preloadedData });
|
||||
}, [preloadedData, reset]);
|
||||
if (isModalOpen) reset({ ...defaultValues, ...preloadedData });
|
||||
}, [preloadedData, reset, isModalOpen]);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isModalOpen} as={Fragment}>
|
||||
@@ -165,4 +167,4 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] };
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.KANBAN}>
|
||||
<>
|
||||
<DeleteIssueModal
|
||||
dataId={draggedIssueId}
|
||||
isOpen={deleteIssueModal}
|
||||
@@ -279,6 +279,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -156,7 +156,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
column_id={subList.id}
|
||||
icon={subList.icon}
|
||||
title={subList.name}
|
||||
count={getGroupIssueCount(subList.id, undefined, false) ?? 0}
|
||||
count={getGroupIssueCount(subList.id, undefined, false)}
|
||||
issuePayload={subList.payload}
|
||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||
addIssuesToView={addIssuesToView}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// hooks
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import isNil from "lodash/isNil";
|
||||
// types
|
||||
|
||||
interface IHeaderGroupByCard {
|
||||
@@ -23,7 +24,7 @@ interface IHeaderGroupByCard {
|
||||
column_id: string;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
count: number;
|
||||
count: number | undefined;
|
||||
kanbanFilters: TIssueKanbanFilters;
|
||||
handleKanbanFilters: any;
|
||||
issuePayload: Partial<TIssue>;
|
||||
@@ -112,7 +113,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`relative flex items-center gap-1 ${
|
||||
className={`relative flex items-baseline gap-1 ${
|
||||
verticalAlignPosition ? `flex-col` : `w-full flex-row overflow-hidden`
|
||||
}`}
|
||||
>
|
||||
@@ -123,11 +124,13 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
|
||||
>
|
||||
{count || 0}
|
||||
</div>
|
||||
{!isNil(count) && (
|
||||
<div
|
||||
className={`flex-shrink-0 text-sm font-medium text-custom-text-300 ${verticalAlignPosition ? `` : `pl-2`}`}
|
||||
>
|
||||
{count || 0}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sub_group_by === null && (
|
||||
|
||||
@@ -28,11 +28,12 @@ import { GroupDragOverlay } from "../group-drag-overlay";
|
||||
import { TRenderQuickActions } from "../list/list-view-types";
|
||||
import { GroupDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload, getIssueBlockId } from "../utils";
|
||||
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface IKanbanGroup {
|
||||
groupId: string;
|
||||
issuesMap: IIssueMap;
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
sub_group_by: TIssueGroupByOptions | undefined;
|
||||
group_by: TIssueGroupByOptions | undefined;
|
||||
@@ -88,8 +89,8 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
const containerRef = sub_group_by && scrollableContainerRef ? scrollableContainerRef : columnRef;
|
||||
|
||||
const loadMoreIssuesInThisGroup = useCallback(() => {
|
||||
loadMoreIssues(groupId, sub_group_id === "null"? undefined: sub_group_id)
|
||||
}, [loadMoreIssues, groupId, sub_group_id])
|
||||
loadMoreIssues(groupId, sub_group_id === "null" ? undefined : sub_group_id);
|
||||
}, [loadMoreIssues, groupId, sub_group_id]);
|
||||
|
||||
const isPaginating = !!getIssueLoader(groupId, sub_group_id);
|
||||
|
||||
@@ -218,11 +219,10 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
? (groupedIssueIds as TSubGroupedIssues)?.[groupId]?.[sub_group_id] ?? []
|
||||
: (groupedIssueIds as TGroupedIssues)?.[groupId] ?? [];
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(groupId, sub_group_id, false) ?? 0;
|
||||
const groupIssueCount = getGroupIssueCount(groupId, sub_group_id, false);
|
||||
|
||||
const nextPageResults = getPaginationData(groupId, sub_group_id)?.nextPageResults;
|
||||
|
||||
|
||||
const loadMore = isPaginating ? (
|
||||
<KanbanIssueBlockLoader />
|
||||
) : (
|
||||
@@ -235,8 +235,9 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const shouldLoadMore = nextPageResults === undefined ? issueIds?.length < groupIssueCount : !!nextPageResults;
|
||||
const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled;
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined ? isNil(groupIssueCount) || issueIds?.length < groupIssueCount : !!nextPageResults;
|
||||
const canOverlayBeVisible = orderBy !== "sort_order" || isDropDisabled;
|
||||
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
|
||||
|
||||
return (
|
||||
@@ -270,7 +271,7 @@ export const KanbanGroup = observer((props: IKanbanGroup) => {
|
||||
canDropOverIssue={!canOverlayBeVisible}
|
||||
/>
|
||||
|
||||
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
|
||||
{shouldLoadMore && (isSubGroup ? <>{loadMore}</> : <KanbanIssueBlockLoader ref={setIntersectionElement} />)}
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||
|
||||
@@ -56,9 +56,9 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
|
||||
{list &&
|
||||
list.length > 0 &&
|
||||
list.map((_list: IGroupByColumn) => {
|
||||
const groupCount = getGroupIssueCount(_list?.id, undefined, false) ?? 0;
|
||||
const groupCount = getGroupIssueCount(_list?.id, undefined, false);
|
||||
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount, showEmptyGroup);
|
||||
const subGroupByVisibilityToggle = visibilitySubGroupByGroupCount(groupCount ?? 0, showEmptyGroup);
|
||||
|
||||
if (subGroupByVisibilityToggle === false) return <></>;
|
||||
|
||||
|
||||
@@ -107,27 +107,25 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.LIST}>
|
||||
<div className={`relative size-full bg-custom-background-90`}>
|
||||
<List
|
||||
issuesMap={issueMap}
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
groupedIssueIds={groupedIssueIds ?? {}}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
quickAddCallback={quickAddIssue}
|
||||
enableIssueQuickAdd={!!enableQuickAdd}
|
||||
canEditProperties={canEditProperties}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
addIssuesToView={addIssuesToView}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
</div>
|
||||
</IssueLayoutHOC>
|
||||
<div className={`relative size-full bg-custom-background-90`}>
|
||||
<List
|
||||
issuesMap={issueMap}
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
groupedIssueIds={groupedIssueIds ?? {}}
|
||||
loadMoreIssues={loadMoreIssues}
|
||||
showEmptyGroup={showEmptyGroup}
|
||||
quickAddCallback={quickAddIssue}
|
||||
enableIssueQuickAdd={!!enableQuickAdd}
|
||||
canEditProperties={canEditProperties}
|
||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||
addIssuesToView={addIssuesToView}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
handleOnDrop={handleOnDrop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -18,12 +18,13 @@ import { cn } from "@/helpers/common.helper";
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
|
||||
import { TSelectionHelper } from "@/hooks/use-multiple-select";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface IHeaderGroupByCard {
|
||||
groupID: string;
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
count: number;
|
||||
count: number | undefined;
|
||||
issuePayload: Partial<TIssue>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
toggleListGroup: () => void;
|
||||
@@ -98,7 +99,7 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
)}
|
||||
groupID={groupID}
|
||||
selectionHelpers={selectionHelpers}
|
||||
disabled={count === 0}
|
||||
disabled={!count}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -106,10 +107,12 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
||||
{icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||
</div>
|
||||
|
||||
<div className="relative flex w-full flex-row items-center gap-1 overflow-hidden cursor-pointer"
|
||||
onClick={toggleListGroup}>
|
||||
<div
|
||||
className="relative flex w-full flex-row items-center gap-1 overflow-hidden cursor-pointer"
|
||||
onClick={toggleListGroup}
|
||||
>
|
||||
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
||||
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||
{!isNil(count) && <div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>}
|
||||
</div>
|
||||
|
||||
{!disableIssueCreation &&
|
||||
|
||||
@@ -37,6 +37,7 @@ import { IssueBlocksList } from "./blocks-list";
|
||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||
import { TRenderQuickActions } from "./list-view-types";
|
||||
import { ListQuickAddIssueForm } from "./quick-add-issue-form";
|
||||
import isNil from "lodash/isNil";
|
||||
|
||||
interface Props {
|
||||
groupIssueIds: string[] | undefined;
|
||||
@@ -98,7 +99,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
|
||||
const [intersectionElement, setIntersectionElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const groupIssueCount = getGroupIssueCount(group.id, undefined, false) ?? 0;
|
||||
const groupIssueCount = getGroupIssueCount(group.id, undefined, false);
|
||||
const nextPageResults = getPaginationData(group.id, undefined)?.nextPageResults;
|
||||
const isPaginating = !!getIssueLoader(group.id);
|
||||
|
||||
@@ -106,7 +107,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
|
||||
const shouldLoadMore =
|
||||
nextPageResults === undefined && groupIssueCount !== undefined && groupIssueIds
|
||||
? groupIssueIds.length < groupIssueCount
|
||||
? isNil(groupIssueCount) || groupIssueIds.length < groupIssueCount
|
||||
: !!nextPageResults;
|
||||
|
||||
const loadMore = isPaginating ? (
|
||||
|
||||
@@ -39,6 +39,7 @@ import { IMemberRootStore } from "@/store/member";
|
||||
import { IModuleStore } from "@/store/module.store";
|
||||
import { IProjectStore } from "@/store/project/project.store";
|
||||
import { IStateStore } from "@/store/state.store";
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
|
||||
export const HIGHLIGHT_CLASS = "highlight";
|
||||
export const HIGHLIGHT_WITH_LINE = "highlight-with-line";
|
||||
@@ -92,8 +93,10 @@ export const getGroupByColumns = (
|
||||
case "created_by":
|
||||
return getCreatedByColumns(member) as any;
|
||||
default:
|
||||
if (includeNone) return [{ id: `All Issues`, name: `All Issues`, payload: {}, icon: undefined }];
|
||||
if (includeNone) return [{ id: ALL_ISSUES, name: ALL_ISSUES, payload: {}, icon: undefined }];
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trash2 } from "lucide-react";
|
||||
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
@@ -80,62 +81,74 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
||||
const { rowData, currentProjectRole, projectId, workspaceSlug } = props;
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
// store hooks
|
||||
const {
|
||||
project: { updateMember },
|
||||
} = useMember();
|
||||
return rowData.role === EUserWorkspaceRoles.ADMIN || currentProjectRole !== EUserProjectRoles.ADMIN ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
if (!workspaceSlug) return;
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
// derived values
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = currentProjectRole === EUserProjectRoles.ADMIN;
|
||||
const isRoleEditable = isCurrentUser && isAdminRole;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
return (
|
||||
<>
|
||||
{isRoleEditable ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), projectId.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserProjectRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,11 +11,10 @@ export const KanbanLayoutLoader = ({ cardsInEachColumn = [2, 3, 2, 4, 3] }: { ca
|
||||
{cardsInEachColumn.map((cardsInColumn, columnIndex) => (
|
||||
<div key={columnIndex} className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between h-9 w-80">
|
||||
<div className="flex item-center">
|
||||
<div className="flex item-center gap-3 px-1.5">
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
|
||||
<span className="h-6 w-24 bg-custom-background-80 rounded animate-pulse" />
|
||||
</div>
|
||||
<span className="h-6 w-6 bg-custom-background-80 rounded animate-pulse" />
|
||||
</div>
|
||||
{Array.from({ length: cardsInColumn }, (_, cardIndex) => (
|
||||
<KanbanIssueBlockLoader key={cardIndex} />
|
||||
|
||||
+5
-5
@@ -31,12 +31,12 @@ export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificat
|
||||
onClick={() => handleFilterTypeChange(value, !isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
|
||||
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
|
||||
)}
|
||||
className={cn("flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all", {
|
||||
"bg-custom-primary text-white": isSelected,
|
||||
"bg-custom-background-90": !isSelected,
|
||||
})}
|
||||
>
|
||||
{isSelected && <Check className="h-2 w-2" />}
|
||||
{isSelected && <Check className="h-2.5 w-2.5" />}
|
||||
</div>
|
||||
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trash2 } from "lucide-react";
|
||||
@@ -6,7 +7,7 @@ import { IUser, IWorkspaceMember } from "@plane/types";
|
||||
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { EUserWorkspaceRoles, ROLE } from "@/constants/workspace";
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { useMember, useUser } from "@/hooks/store";
|
||||
|
||||
export interface RowData {
|
||||
member: IWorkspaceMember;
|
||||
@@ -79,63 +80,75 @@ export const NameColumn: React.FC<NameProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = (props) => {
|
||||
export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) => {
|
||||
const { rowData, currentWorkspaceRole, workspaceSlug } = props;
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
// store hooks
|
||||
const {
|
||||
workspace: { updateMember },
|
||||
} = useMember();
|
||||
return rowData.role === EUserWorkspaceRoles.ADMIN || currentWorkspaceRole !== EUserWorkspaceRoles.ADMIN ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
console.log({ value, workspaceSlug }, "onChange");
|
||||
if (!workspaceSlug) return;
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
updateMember(workspaceSlug.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
// derived values
|
||||
const isCurrentUser = currentUser?.id === rowData.member.id;
|
||||
const isAdminRole = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
const isRoleEditable = isCurrentUser && isAdminRole;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
return (
|
||||
<>
|
||||
{isRoleEditable ? (
|
||||
<div className="w-32 flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
) : (
|
||||
<Controller
|
||||
name="role"
|
||||
control={control}
|
||||
rules={{ required: "Role is required." }}
|
||||
render={({ field: { value } }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
onChange={(value: EUserProjectRoles) => {
|
||||
console.log({ value, workspaceSlug }, "onChange");
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
updateMember(workspaceSlug.toString(), rowData.member.id, {
|
||||
role: value as unknown as EUserWorkspaceRoles, // Cast value to unknown first, then to EUserWorkspaceRoles
|
||||
}).catch((err) => {
|
||||
console.log(err, "err");
|
||||
const error = err.error;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: errorString ?? "An error occurred while updating member role. Please try again.",
|
||||
});
|
||||
});
|
||||
}}
|
||||
label={
|
||||
<div className="flex ">
|
||||
<span>{ROLE[rowData.role as keyof typeof ROLE]}</span>
|
||||
</div>
|
||||
}
|
||||
buttonClassName={`!px-0 !justify-start hover:bg-custom-background-100 ${errors.role ? "border-red-500" : "border-none"}`}
|
||||
className="rounded-md p-0 w-32"
|
||||
optionsClassName="w-full"
|
||||
input
|
||||
>
|
||||
{Object.keys(ROLE).map((item) => (
|
||||
<CustomSelect.Option key={item} value={item as unknown as EUserProjectRoles}>
|
||||
{ROLE[item as unknown as keyof typeof ROLE]}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -59,37 +59,37 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
workspaceSlug && projectId ? () => fetchUserProjectInfo(workspaceSlug.toString(), projectId.toString()) : null
|
||||
);
|
||||
// fetching project labels
|
||||
useSWR(
|
||||
const { isLoading: isLabelsLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project members
|
||||
useSWR(
|
||||
const { isLoading: isMembersLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project states
|
||||
useSWR(
|
||||
const { isLoading: isStateLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project estimates
|
||||
useSWR(
|
||||
const { isLoading: isEstimatesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => getProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project cycles
|
||||
useSWR(
|
||||
const { isLoading: isCyclesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
// fetching project modules
|
||||
useSWR(
|
||||
const { isLoading: isModulesLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null,
|
||||
workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
@@ -102,8 +102,11 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
|
||||
);
|
||||
const projectExists = projectId ? getProjectById(projectId.toString()) : null;
|
||||
|
||||
const isLoading =
|
||||
isLabelsLoading || isMembersLoading || isStateLoading || isEstimatesLoading || isCyclesLoading || isModulesLoading;
|
||||
|
||||
// check if the project member apis is loading
|
||||
if (!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null)
|
||||
if ((!projectMemberInfo && projectId && hasPermissionToProject[projectId.toString()] === null) || isLoading)
|
||||
return (
|
||||
<div className="grid h-screen place-items-center bg-custom-background-100 p-4">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
getSubGroupIssueKeyActions,
|
||||
} from "./base-issues-utils";
|
||||
import { IBaseIssueFilterStore } from "./issue-filter-helper.store";
|
||||
import { getGroupByColumns } from "@/components/issues/issue-layouts/utils";
|
||||
// constants
|
||||
// helpers
|
||||
// services
|
||||
@@ -419,6 +420,56 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
}
|
||||
);
|
||||
|
||||
getGroups = (isWorkspaceLevel = false) => {
|
||||
if (!this.groupBy || this.groupBy === "target_date") return;
|
||||
|
||||
const {
|
||||
projectRoot,
|
||||
cycle,
|
||||
module: moduleInfo,
|
||||
label,
|
||||
state: projectState,
|
||||
memberRoot,
|
||||
} = this.rootIssueStore.rootStore;
|
||||
|
||||
return getGroupByColumns(
|
||||
this.groupBy,
|
||||
projectRoot.project,
|
||||
cycle,
|
||||
moduleInfo,
|
||||
label,
|
||||
projectState,
|
||||
memberRoot,
|
||||
false,
|
||||
isWorkspaceLevel
|
||||
);
|
||||
};
|
||||
|
||||
getSubGroups = (isWorkspaceLevel = false) => {
|
||||
if (!this.subGroupBy || this.subGroupBy === "target_date") return;
|
||||
|
||||
const {
|
||||
projectRoot,
|
||||
cycle,
|
||||
module: moduleInfo,
|
||||
label,
|
||||
state: projectState,
|
||||
memberRoot,
|
||||
} = this.rootIssueStore.rootStore;
|
||||
|
||||
return getGroupByColumns(
|
||||
this.subGroupBy,
|
||||
projectRoot.project,
|
||||
cycle,
|
||||
moduleInfo,
|
||||
label,
|
||||
projectState,
|
||||
memberRoot,
|
||||
false,
|
||||
isWorkspaceLevel
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the next page cursor based on number of issues currently available
|
||||
* @param groupId groupId for the cursor
|
||||
@@ -451,9 +502,8 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
onfetchIssues(
|
||||
issuesResponse: TIssuesResponse,
|
||||
options: IssuePaginationOptions,
|
||||
workspaceSlug: string,
|
||||
projectId?: string,
|
||||
id?: string
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) {
|
||||
// Process the Issue Response to get the following data from it
|
||||
const { issueList, groupedIssues, groupedIssueCount } = this.processIssueResponse(issuesResponse);
|
||||
@@ -463,15 +513,12 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
// Update all the GroupIds to this Store's groupedIssueIds and update Individual group issue counts
|
||||
runInAction(() => {
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount);
|
||||
this.loader[getGroupKey()] = undefined;
|
||||
this.updateGroupedIssueIds(groupedIssues, groupedIssueCount, groupId, subGroupId);
|
||||
this.loader[getGroupKey(groupId, subGroupId)] = undefined;
|
||||
});
|
||||
|
||||
// fetch parent stats if required, to be handled in the Implemented class
|
||||
this.fetchParentStats(workspaceSlug, projectId, id);
|
||||
|
||||
// store Pagination options for next subsequent calls and data like next cursor etc
|
||||
this.storePreviousPaginationValues(issuesResponse, options);
|
||||
this.storePreviousPaginationValues(issuesResponse, options, groupId, subGroupId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -892,9 +939,14 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
|
||||
|
||||
addCycleToIssue = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||
const issueCycleId = this.rootIssueStore.issues.getIssueById(issueId)?.cycle_id;
|
||||
|
||||
if (issueCycleId === cycleId) return;
|
||||
|
||||
try {
|
||||
// Update issueIds from current store
|
||||
runInAction(() => {
|
||||
// If cycle Id before update is the same as current cycle Id then, remove issueId from list
|
||||
if (this.cycleId === issueCycleId) this.removeIssueFromList(issueId);
|
||||
// If cycle Id is the current cycle Id, then, add issue to list of issueIds
|
||||
if (this.cycleId === cycleId) this.addIssueToList(issueId);
|
||||
// For Each issue update cycle Id by calling current store's update Issue, without making an API call
|
||||
|
||||
@@ -282,7 +282,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
subGroupId?: string
|
||||
) {
|
||||
// if cursor exists, use the cursor. If it doesn't exist construct the cursor based on per page count
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:1:0` : `${options.perPageCount}:0:0`;
|
||||
const pageCursor = cursor ? cursor : groupId ? `${options.perPageCount}:0:0` : `${options.perPageCount}:0:0`;
|
||||
|
||||
// pagination params
|
||||
const paginationParams: Partial<Record<TIssueParams, string | boolean>> = {
|
||||
|
||||
@@ -86,21 +86,74 @@ export class ProjectIssues extends BaseIssuesStore implements IProjectIssues {
|
||||
isExistingPaginationOptions: boolean = false
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
this.setLoader(loadType);
|
||||
});
|
||||
const groups = this.getGroups();
|
||||
const subGroups = this.getSubGroups();
|
||||
|
||||
this.clear(!isExistingPaginationOptions);
|
||||
|
||||
if (!groups || (!groups && !subGroups))
|
||||
return await this.fetchParallelIssues(workspaceSlug, projectId, loadType, options);
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const group of groups) {
|
||||
if (!subGroups) {
|
||||
promises.push(this.fetchParallelIssues(workspaceSlug, projectId, loadType, options, group.id));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const subGroup of subGroups) {
|
||||
promises.push(this.fetchParallelIssues(workspaceSlug, projectId, loadType, options, group.id, subGroup.id));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// fetch parent stats if required, to be handled in the Implemented class
|
||||
this.fetchParentStats(workspaceSlug, projectId);
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.setLoader(undefined);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called to fetch the first issues of pagination
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param loadType
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
fetchParallelIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
loadType: TLoader = "init-loader",
|
||||
options: IssuePaginationOptions,
|
||||
groupId?: string,
|
||||
subGroupId?: string
|
||||
) => {
|
||||
try {
|
||||
// set loader and clear store
|
||||
runInAction(() => {
|
||||
// set Loader
|
||||
if (groupId || subGroupId) {
|
||||
this.setLoader("pagination", groupId, subGroupId);
|
||||
} else {
|
||||
this.setLoader(loadType);
|
||||
}
|
||||
});
|
||||
|
||||
// get params from pagination options
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, undefined, undefined);
|
||||
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, groupId, subGroupId);
|
||||
// call the fetch issues API with the params
|
||||
const response = await this.issueService.getIssues(workspaceSlug, projectId, params, {
|
||||
signal: this.controller.signal,
|
||||
});
|
||||
|
||||
// after fetching issues, call the base method to process the response further
|
||||
this.onfetchIssues(response, options, workspaceSlug, projectId);
|
||||
this.onfetchIssues(response, options, groupId, subGroupId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
|
||||
Reference in New Issue
Block a user