Compare commits

...
22 changed files with 327 additions and 183 deletions
@@ -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);
+1 -1
View File
@@ -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} />
@@ -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>> = {
+59 -6
View File
@@ -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