diff --git a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
index 09282743f3..7dc7fefb39 100644
--- a/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
+++ b/web/components/issues/issue-layouts/filters/applied-filters/roots/global-view-root.tsx
@@ -96,12 +96,13 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
...(appliedFilters ?? {}),
},
}).then((res) => {
- captureEvent(GLOBAL_VIEW_UPDATED, {
- view_id: res.id,
- applied_filters: res.filters,
- state: "SUCCESS",
- element: "Spreadsheet view",
- });
+ if (res)
+ captureEvent(GLOBAL_VIEW_UPDATED, {
+ view_id: res.id,
+ applied_filters: res.filters,
+ state: "SUCCESS",
+ element: "Spreadsheet view",
+ });
});
};
diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
index 0d394cbaae..b33a746601 100644
--- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
+++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx
@@ -75,6 +75,8 @@ export const BaseKanBanRoot: React.FC
= observer((props: IBas
const sub_group_by = displayFilters?.sub_group_by;
const group_by = displayFilters?.group_by;
+ const orderBy = displayFilters?.order_by;
+
const userDisplayFilters = displayFilters || null;
const KanBanView = sub_group_by ? KanBanSwimLanes : KanBan;
@@ -157,7 +159,8 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
issues.getIssueIds,
updateIssue,
group_by,
- sub_group_by
+ sub_group_by,
+ orderBy !== "sort_order"
).catch((err) => {
setToast({
title: "Error",
@@ -259,6 +262,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
+ orderBy={orderBy}
updateIssue={updateIssue}
quickActions={renderQuickActions}
handleKanbanFilters={handleKanbanFilters}
diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx
index 82e7dc19c9..752cb670c9 100644
--- a/web/components/issues/issue-layouts/kanban/block.tsx
+++ b/web/components/issues/issue-layouts/kanban/block.tsx
@@ -4,10 +4,11 @@ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-d
import { observer } from "mobx-react-lite";
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
// hooks
-import { ControlLink, DropIndicator, Tooltip } from "@plane/ui";
+import { ControlLink, DropIndicator, TOAST_TYPE, Tooltip, setToast } from "@plane/ui";
import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { cn } from "@/helpers/common.helper";
import { useApplication, useIssueDetail, useKanbanView, useProject } from "@/hooks/store";
+import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
// components
import { TRenderQuickActions } from "../list/list-view-types";
@@ -131,6 +132,10 @@ export const KanbanIssueBlock: React.FC = observer((props) => {
const isDragAllowed = !isDragDisabled && !issue?.tempId && canEditIssueProperties;
+ useOutsideClickDetector(cardRef, () => {
+ cardRef?.current?.classList?.remove("highlight");
+ });
+
// Make Issue block both as as Draggable and,
// as a DropTarget for other issues being dragged to get the location of drop
useEffect(() => {
@@ -177,7 +182,15 @@ export const KanbanIssueBlock: React.FC = observer((props) => {
isDragAllowed && setIsCurrentBlockDragging(true)}
+ onDragStart={() => {
+ if (isDragAllowed) setIsCurrentBlockDragging(true);
+ else
+ setToast({
+ type: TOAST_TYPE.WARNING,
+ title: "Cannot move issue",
+ message: "Drag and drop is disabled for the current grouping",
+ });
+ }}
>
= observer((props) => {
}`}
ref={cardRef}
className={cn(
- "block rounded border-[0.5px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
- {
- "hover:cursor-pointer": isDragAllowed,
- "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id),
- "bg-custom-background-80 z-[100]": isCurrentBlockDragging,
- }
+ "block rounded border-[1px] outline-[0.5px] outline-transparent w-full border-custom-border-200 bg-custom-background-100 text-sm transition-all hover:border-custom-border-400",
+ { "hover:cursor-pointer": isDragAllowed },
+ { "border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issue.id) },
+ { "bg-custom-background-80 z-[100]": isCurrentBlockDragging }
)}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx
index 82aec5c5aa..6378339ac8 100644
--- a/web/components/issues/issue-layouts/kanban/default.tsx
+++ b/web/components/issues/issue-layouts/kanban/default.tsx
@@ -11,6 +11,7 @@ import {
TUnGroupedIssues,
TIssueKanbanFilters,
TIssueGroupByOptions,
+ TIssueOrderByOptions,
} from "@plane/types";
// constants
// hooks
@@ -31,6 +32,7 @@ export interface IGroupByKanBan {
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
+ orderBy: TIssueOrderByOptions | undefined;
sub_group_id: string;
isDragDisabled: boolean;
updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined;
@@ -79,6 +81,7 @@ const GroupByKanBan: React.FC = observer((props) => {
handleOnDrop,
showEmptyGroup = true,
subGroupIssueHeaderCount,
+ orderBy,
} = props;
const member = useMember();
@@ -170,6 +173,7 @@ const GroupByKanBan: React.FC = observer((props) => {
displayProperties={displayProperties}
sub_group_by={sub_group_by}
group_by={group_by}
+ orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
updateIssue={updateIssue}
@@ -196,6 +200,7 @@ export interface IKanBan {
displayProperties: IIssueDisplayProperties | undefined;
sub_group_by: TIssueGroupByOptions | undefined;
group_by: TIssueGroupByOptions | undefined;
+ orderBy: TIssueOrderByOptions | undefined;
sub_group_id?: string;
updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined;
quickActions: TRenderQuickActions;
@@ -242,6 +247,7 @@ export const KanBan: React.FC = observer((props) => {
handleOnDrop,
showEmptyGroup,
subGroupIssueHeaderCount,
+ orderBy,
} = props;
const issueKanBanView = useKanbanView();
@@ -253,6 +259,7 @@ export const KanBan: React.FC = observer((props) => {
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
+ orderBy={orderBy}
sub_group_id={sub_group_id}
isDragDisabled={!issueKanBanView?.getCanUserDragDrop(group_by, sub_group_by)}
updateIssue={updateIssue}
diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx
index 85f24d90b9..c9e04c819d 100644
--- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx
+++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx
@@ -11,14 +11,21 @@ import {
TSubGroupedIssues,
TUnGroupedIssues,
TIssueGroupByOptions,
+ TIssueOrderByOptions,
} from "@plane/types";
+import { ISSUE_ORDER_BY_OPTIONS } from "@/constants/issue";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useProjectState } from "@/hooks/store";
//components
import { TRenderQuickActions } from "../list/list-view-types";
-import { KanbanDropLocation, getSourceFromDropPayload, getDestinationFromDropPayload } from "./utils";
+import {
+ KanbanDropLocation,
+ getSourceFromDropPayload,
+ getDestinationFromDropPayload,
+ highlightIssueOnDrop,
+} from "./utils";
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
interface IKanbanGroup {
@@ -45,6 +52,7 @@ interface IKanbanGroup {
groupByVisibilityToggle?: boolean;
scrollableContainerRef?: MutableRefObject;
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise;
+ orderBy: TIssueOrderByOptions | undefined;
}
export const KanbanGroup = (props: IKanbanGroup) => {
@@ -52,6 +60,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
groupId,
sub_group_id,
group_by,
+ orderBy,
sub_group_by,
issuesMap,
displayProperties,
@@ -101,13 +110,15 @@ export const KanbanGroup = (props: IKanbanGroup) => {
if (!source || !destination) return;
handleOnDrop(source, destination);
+
+ highlightIssueOnDrop(payload.source.element.id, orderBy !== "sort_order");
},
}),
autoScrollForElements({
element,
})
);
- }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn]);
+ }, [columnRef?.current, groupId, sub_group_id, setIsDraggingOverColumn, orderBy]);
const prePopulateQuickAddData = (
groupByKey: string | undefined,
@@ -161,16 +172,33 @@ export const KanbanGroup = (props: IKanbanGroup) => {
return preloadedData;
};
+ const shouldOverlay = isDraggingOverColumn && orderBy !== "sort_order";
+ const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.title;
+
return (
+
+ {readableOrderBy && The layout is ordered by {readableOrderBy}.}
+ Drop here to move the issue.
+
{
updateIssue={updateIssue}
quickActions={quickActions}
canEditProperties={canEditProperties}
- scrollableContainerRef={scrollableContainerRef}
+ scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef}
/>
{enableQuickIssueCreate && !disableIssueCreation && (
diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx
index ae881b9ede..5669ac3fb5 100644
--- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx
+++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx
@@ -11,6 +11,7 @@ import {
TUnGroupedIssues,
TIssueKanbanFilters,
TIssueGroupByOptions,
+ TIssueOrderByOptions,
} from "@plane/types";
// components
import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "@/hooks/store";
@@ -114,6 +115,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
disableIssueCreation?: boolean;
storeType: KanbanStoreType;
enableQuickIssueCreate: boolean;
+ orderBy: TIssueOrderByOptions | undefined;
canEditProperties: (projectId: string | undefined) => boolean;
addIssuesToView?: (issueIds: string[]) => Promise;
quickAddCallback?: (
@@ -146,6 +148,7 @@ const SubGroupSwimlane: React.FC = observer((props) => {
viewId,
scrollableContainerRef,
handleOnDrop,
+ orderBy,
} = props;
const calculateIssueCount = (column_id: string) => {
@@ -181,7 +184,7 @@ const SubGroupSwimlane: React.FC = observer((props) => {
if (subGroupByVisibilityToggle.showGroup === false) return <>>;
return (
-
+
= observer((props) => {
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
handleOnDrop={handleOnDrop}
+ orderBy={orderBy}
subGroupIssueHeaderCount={(groupByListId: string) =>
getSubGroupHeaderIssuesCount(issueIds as TSubGroupedIssues, groupByListId)
}
@@ -254,6 +258,7 @@ export interface IKanBanSwimLanes {
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject;
+ orderBy: TIssueOrderByOptions | undefined;
}
export const KanBanSwimLanes: React.FC = observer((props) => {
@@ -263,6 +268,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => {
displayProperties,
sub_group_by,
group_by,
+ orderBy,
updateIssue,
storeType,
quickActions,
@@ -313,7 +319,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => {
return (
-
+
= observer((props) => {
displayProperties={displayProperties}
group_by={group_by}
sub_group_by={sub_group_by}
+ orderBy={orderBy}
updateIssue={updateIssue}
quickActions={quickActions}
kanbanFilters={kanbanFilters}
diff --git a/web/components/issues/issue-layouts/kanban/utils.ts b/web/components/issues/issue-layouts/kanban/utils.ts
index 3981455dbc..cf4745f63b 100644
--- a/web/components/issues/issue-layouts/kanban/utils.ts
+++ b/web/components/issues/issue-layouts/kanban/utils.ts
@@ -1,4 +1,5 @@
import pull from "lodash/pull";
+import scrollIntoView from "smooth-scroll-into-view-if-needed";
import { IPragmaticDropPayload, TIssue, TIssueGroupByOptions } from "@plane/types";
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/issue-helper.store";
@@ -87,14 +88,17 @@ export const getDestinationFromDropPayload = (payload: IPragmaticDropPayload): K
const handleSortOrder = (
destinationIssues: string[],
destinationIssueId: string | undefined,
- getIssueById: (issueId: string) => TIssue | undefined
+ getIssueById: (issueId: string) => TIssue | undefined,
+ shouldAddIssueAtTop = false
) => {
const sortOrderDefaultValue = 65535;
let currentIssueState = {};
const destinationIndex = destinationIssueId
? destinationIssues.indexOf(destinationIssueId)
- : destinationIssues.length;
+ : shouldAddIssueAtTop
+ ? 0
+ : destinationIssues.length;
if (destinationIssues && destinationIssues.length > 0) {
if (destinationIndex === 0) {
@@ -145,7 +149,8 @@ export const handleDragDrop = async (
getIssueIds: (groupId?: string, subGroupId?: string) => string[] | undefined,
updateIssue: ((projectId: string, issueId: string, data: Partial) => Promise) | undefined,
groupBy: TIssueGroupByOptions | undefined,
- subGroupBy: TIssueGroupByOptions | undefined
+ subGroupBy: TIssueGroupByOptions | undefined,
+ shouldAddIssueAtTop = false
) => {
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
@@ -165,7 +170,7 @@ export const handleDragDrop = async (
// for both horizontal and vertical dnd
updatedIssue = {
...updatedIssue,
- ...handleSortOrder(destinationIssues, destination.id, getIssueById),
+ ...handleSortOrder(destinationIssues, destination.id, getIssueById, shouldAddIssueAtTop),
};
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
@@ -207,3 +212,18 @@ export const handleDragDrop = async (
);
}
};
+
+/**
+ * This Method finds the DOM element with elementId, scrolls to it and highlights the issue block
+ * @param elementId
+ * @param shouldScrollIntoView
+ */
+export const highlightIssueOnDrop = (elementId: string | undefined, shouldScrollIntoView = true) => {
+ setTimeout(async () => {
+ const sourceElementId = elementId ?? "";
+ const sourceElement = document.getElementById(sourceElementId);
+ sourceElement?.classList?.add("highlight");
+ if (shouldScrollIntoView && sourceElement)
+ await scrollIntoView(sourceElement, { behavior: "smooth", block: "center", duration: 1500 });
+ }, 200);
+};
diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx
index 6c10c39dc6..59565eb56f 100644
--- a/web/components/issues/issue-modal/form.tsx
+++ b/web/components/issues/issue-modal/form.tsx
@@ -480,7 +480,7 @@ export const IssueFormRoot: FC = observer((props) => {
ref={editorRef}
tabIndex={getTabIndex("description_html")}
placeholder={getDescriptionPlaceholder}
- containerClassName="border-[0.5px] border-custom-border-200 py-3"
+ containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
/>
)}
/>
diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx
index e2ad668ba1..5cbe694146 100644
--- a/web/components/issues/peek-overview/header.tsx
+++ b/web/components/issues/peek-overview/header.tsx
@@ -54,7 +54,7 @@ export type PeekOverviewHeaderProps = {
isArchived: boolean;
disabled: boolean;
toggleDeleteIssueModal: (issueId: string | null) => void;
- toggleArchiveIssueModal: (value: boolean) => void;
+ toggleArchiveIssueModal: (issueId: string | null) => void;
handleRestoreIssue: () => void;
isSubmitting: "submitting" | "submitted" | "saved";
};
@@ -178,7 +178,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr
})}
onClick={() => {
if (!isInArchivableGroup) return;
- toggleArchiveIssueModal(true);
+ toggleArchiveIssueModal(issueId);
}}
>
diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx
index a136252650..6d4718fe74 100644
--- a/web/components/issues/peek-overview/issue-detail.tsx
+++ b/web/components/issues/peek-overview/issue-detail.tsx
@@ -82,7 +82,7 @@ export const PeekOverviewIssueDetails: FC = observer(
disabled={disabled}
issueOperations={issueOperations}
setIsSubmitting={(value) => setIsSubmitting(value)}
- containerClassName="-ml-3 border-none"
+ containerClassName="-ml-3 !mb-6 border-none"
/>
{currentUser && (
diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx
index aaa150674a..3aefa0438f 100644
--- a/web/components/issues/peek-overview/view.tsx
+++ b/web/components/issues/peek-overview/view.tsx
@@ -87,8 +87,8 @@ export const IssueView: FC = observer((props) => {
<>
{issue && !is_archived && (
toggleArchiveIssueModal(false)}
+ isOpen={isArchiveIssueModalOpen === issueId}
+ handleClose={() => toggleArchiveIssueModal(null)}
data={issue}
onSubmit={async () => {
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx
index 25cbc2b176..deff31f1e0 100644
--- a/web/components/issues/sub-issues/root.tsx
+++ b/web/components/issues/sub-issues/root.tsx
@@ -381,7 +381,7 @@ export const SubIssuesRoot: FC = observer((props) => {
onClick={() => {
setTrackElement("Issue detail nested sub-issue");
handleIssueCrudState("existing", parentIssueId, null);
- toggleSubIssuesModal(true);
+ toggleSubIssuesModal(issue.id);
}}
>
@@ -439,7 +439,7 @@ export const SubIssuesRoot: FC
= observer((props) => {
onClick={() => {
setTrackElement("Issue detail nested sub-issue");
handleIssueCrudState("existing", parentIssueId, null);
- toggleSubIssuesModal(true);
+ toggleSubIssuesModal(issue.id);
}}
>
@@ -476,7 +476,7 @@ export const SubIssuesRoot: FC
= observer((props) => {
isOpen={issueCrudState?.existing?.toggle}
handleClose={() => {
handleIssueCrudState("existing", null, null);
- toggleSubIssuesModal(false);
+ toggleSubIssuesModal(null);
}}
searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }}
handleOnSubmit={(_issue) =>
diff --git a/web/components/workspace/views/modal.tsx b/web/components/workspace/views/modal.tsx
index b9eee70040..15f94ced59 100644
--- a/web/components/workspace/views/modal.tsx
+++ b/web/components/workspace/views/modal.tsx
@@ -84,17 +84,19 @@ export const CreateUpdateWorkspaceViewModal: React.FC = observer((props)
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
.then((res) => {
- captureEvent(GLOBAL_VIEW_UPDATED, {
- view_id: res.id,
- applied_filters: res.filters,
- state: "SUCCESS",
- });
- setToast({
- type: TOAST_TYPE.SUCCESS,
- title: "Success!",
- message: "View updated successfully.",
- });
- handleClose();
+ if (res) {
+ captureEvent(GLOBAL_VIEW_UPDATED, {
+ view_id: res.id,
+ applied_filters: res.filters,
+ state: "SUCCESS",
+ });
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Success!",
+ message: "View updated successfully.",
+ });
+ handleClose();
+ }
})
.catch(() => {
captureEvent(GLOBAL_VIEW_UPDATED, {
diff --git a/web/constants/issue.ts b/web/constants/issue.ts
index 3bae30d2fd..9ec163935e 100644
--- a/web/constants/issue.ts
+++ b/web/constants/issue.ts
@@ -336,7 +336,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
calendar: {
filters: ["priority", "state", "cycle", "module", "assignees", "mentions", "created_by", "labels", "start_date"],
- display_properties: true,
+ display_properties: false,
display_filters: {
type: [null, "active", "backlog"],
},
diff --git a/web/helpers/date-time.helper.ts b/web/helpers/date-time.helper.ts
index 5d5acd07e1..a6719ec1e3 100644
--- a/web/helpers/date-time.helper.ts
+++ b/web/helpers/date-time.helper.ts
@@ -224,7 +224,29 @@ export const getDate = (date: string | Date | undefined | null): Date | undefine
return undefined;
}
};
+
export const isInDateFormat = (date: string) => {
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
return datePattern.test(date);
};
+
+/**
+ * returns the date string in ISO format regardless of the timezone in input date string
+ * @param dateString
+ * @returns
+ */
+export const convertToISODateString = (dateString: string | undefined) => {
+ if (!dateString) return dateString;
+
+ const date = new Date(dateString);
+ return date.toISOString();
+};
+
+/**
+ * get current Date time in UTC ISO format
+ * @returns
+ */
+export const getCurrentDateTimeInISO = () => {
+ const date = new Date();
+ return date.toISOString();
+};
\ No newline at end of file
diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx
index 174cfdd8ae..cc69906ce0 100644
--- a/web/hooks/use-dropdown-key-down.tsx
+++ b/web/hooks/use-dropdown-key-down.tsx
@@ -9,23 +9,25 @@ type TUseDropdownKeyDown = {
};
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
- const stopEventPropagation = (event: React.KeyboardEvent) => {
- if (stopPropagation) {
- event.stopPropagation();
- event.preventDefault();
- }
- };
+ const stopEventPropagation = useCallback(
+ (event: React.KeyboardEvent) => {
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ },
+ [stopPropagation]
+ );
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
stopEventPropagation(event);
-
onEnterKeyDown();
} else if (event.key === "Escape") {
stopEventPropagation(event);
onEscKeyDown();
- }
+ } else if (event.key === "Tab") onEscKeyDown();
},
[onEnterKeyDown, onEscKeyDown, stopEventPropagation]
);
diff --git a/web/package.json b/web/package.json
index a4c94d051f..246ccf6ddf 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "web",
- "version": "0.18.0",
+ "version": "0.19.0",
"private": true,
"scripts": {
"dev": "turbo run develop",
@@ -58,6 +58,7 @@
"react-markdown": "^8.0.7",
"react-popper": "^2.3.0",
"sharp": "^0.32.1",
+ "smooth-scroll-into-view-if-needed": "^2.0.2",
"swr": "^2.1.3",
"tailwind-merge": "^2.0.0",
"use-debounce": "^9.0.4",
diff --git a/web/store/global-view.store.ts b/web/store/global-view.store.ts
index c6d3267845..742d7af328 100644
--- a/web/store/global-view.store.ts
+++ b/web/store/global-view.store.ts
@@ -1,11 +1,16 @@
-import { set } from "lodash";
+import cloneDeep from "lodash/cloneDeep";
+import isEmpty from "lodash/isEmpty";
+import isEqual from "lodash/isEqual";
+import set from "lodash/set";
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
+import { IIssueFilterOptions, IWorkspaceView } from "@plane/types";
+// constants
+import { EIssueFilterType } from "@/constants/issue";
// services
import { WorkspaceService } from "@/services/workspace.service";
// types
import { RootStore } from "@/store/root.store";
-import { IWorkspaceView } from "@plane/types";
export interface IGlobalViewStore {
// observables
@@ -20,7 +25,11 @@ export interface IGlobalViewStore {
fetchGlobalViewDetails: (workspaceSlug: string, viewId: string) => Promise;
// crud actions
createGlobalView: (workspaceSlug: string, data: Partial) => Promise;
- updateGlobalView: (workspaceSlug: string, viewId: string, data: Partial) => Promise;
+ updateGlobalView: (
+ workspaceSlug: string,
+ viewId: string,
+ data: Partial
+ ) => Promise;
deleteGlobalView: (workspaceSlug: string, viewId: string) => Promise;
}
@@ -139,14 +148,50 @@ export class GlobalViewStore implements IGlobalViewStore {
workspaceSlug: string,
viewId: string,
data: Partial
- ): Promise =>
- await this.workspaceService.updateView(workspaceSlug, viewId, data).then((response) => {
- const viewToUpdate = { ...this.getViewDetailsById(viewId), ...data };
- runInAction(() => {
- set(this.globalViewMap, viewId, viewToUpdate);
+ ): Promise => {
+ const currentViewData = this.getViewDetailsById(viewId) ? cloneDeep(this.getViewDetailsById(viewId)) : undefined;
+ try {
+ Object.keys(data).forEach((key) => {
+ const currentKey = key as keyof IWorkspaceView;
+ set(this.globalViewMap, [viewId, currentKey], data[currentKey]);
});
- return response;
- });
+ const currentView = await this.workspaceService.updateView(workspaceSlug, viewId, data);
+ // applying the filters in the global view
+ if (!isEqual(currentViewData?.filters || {}, currentView?.filters || {})) {
+ if (isEmpty(currentView?.filters)) {
+ const currentGlobalViewFilters: IIssueFilterOptions = this.rootStore.issue.workspaceIssuesFilter.filters[
+ viewId
+ ].filters as IIssueFilterOptions;
+ const newFilters: IIssueFilterOptions = {};
+ Object.keys(currentGlobalViewFilters ?? {}).forEach((key) => {
+ newFilters[key as keyof IIssueFilterOptions] = [];
+ });
+ await this.rootStore.issue.workspaceIssuesFilter.updateFilters(
+ workspaceSlug,
+ undefined,
+ EIssueFilterType.FILTERS,
+ newFilters,
+ viewId
+ );
+ } else {
+ await this.rootStore.issue.workspaceIssuesFilter.updateFilters(
+ workspaceSlug,
+ undefined,
+ EIssueFilterType.FILTERS,
+ currentView.filters,
+ viewId
+ );
+ }
+ this.rootStore.issue.workspaceIssues.fetchIssues(workspaceSlug, viewId, "mutation");
+ }
+ return currentView;
+ } catch {
+ Object.keys(data).forEach((key) => {
+ const currentKey = key as keyof IWorkspaceView;
+ if (currentViewData) set(this.globalViewMap, [viewId, currentKey], currentViewData[currentKey]);
+ });
+ }
+ };
/**
* @description delete global view
diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts
index 3876d675cf..e308679cad 100644
--- a/web/store/issue/helpers/issue-helper.store.ts
+++ b/web/store/issue/helpers/issue-helper.store.ts
@@ -7,7 +7,7 @@ import values from "lodash/values";
import { ISSUE_PRIORITIES } from "@/constants/issue";
import { STATE_GROUPS } from "@/constants/state";
// helpers
-import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
+import { convertToISODateString, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
// types
import { TIssue, TIssueMap, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
// store
@@ -262,13 +262,13 @@ export class IssueHelperStore implements TIssueHelperStore {
return orderBy(array, (issue) => this.populateIssueDataForSorting("state_id", issue["state_id"]), ["desc"]);
// dates
case "created_at":
- return orderBy(array, "created_at");
+ return orderBy(array, (issue) => convertToISODateString(issue["created_at"]));
case "-created_at":
- return orderBy(array, "created_at", ["desc"]);
+ return orderBy(array, (issue) => convertToISODateString(issue["created_at"]), ["desc"]);
case "updated_at":
- return orderBy(array, "updated_at");
+ return orderBy(array, (issue) => convertToISODateString(issue["updated_at"]));
case "-updated_at":
- return orderBy(array, "updated_at", ["desc"]);
+ return orderBy(array, (issue) => convertToISODateString(issue["updated_at"]), ["desc"]);
case "start_date":
return orderBy(array, [this.getSortOrderToFilterEmptyValues.bind(null, "start_date"), "start_date"]); //preferring sorting based on empty values to always keep the empty values below
case "-start_date":
diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts
index 853cef2389..c2d8908060 100644
--- a/web/store/issue/issue-details/root.store.ts
+++ b/web/store/issue/issue-details/root.store.ts
@@ -31,6 +31,11 @@ export type TPeekIssue = {
issueId: string;
};
+export type TIssueRelationModal = {
+ issueId: string | null;
+ relationType: TIssueRelationTypes | null;
+};
+
export interface IIssueDetail
extends IIssueStoreActions,
IIssueReactionStoreActions,
@@ -46,11 +51,11 @@ export interface IIssueDetail
peekIssue: TPeekIssue | undefined;
isCreateIssueModalOpen: boolean;
isIssueLinkModalOpen: boolean;
- isParentIssueModalOpen: boolean;
+ isParentIssueModalOpen: string | null;
isDeleteIssueModalOpen: string | null;
- isArchiveIssueModalOpen: boolean;
- isRelationModalOpen: TIssueRelationTypes | null;
- isSubIssuesModalOpen: boolean;
+ isArchiveIssueModalOpen: string | null;
+ isRelationModalOpen: TIssueRelationModal | null;
+ isSubIssuesModalOpen: string | null;
isDeleteAttachmentModalOpen: string | null;
// computed
isAnyModalOpen: boolean;
@@ -60,11 +65,11 @@ export interface IIssueDetail
setPeekIssue: (peekIssue: TPeekIssue | undefined) => void;
toggleCreateIssueModal: (value: boolean) => void;
toggleIssueLinkModal: (value: boolean) => void;
- toggleParentIssueModal: (value: boolean) => void;
+ toggleParentIssueModal: (issueId: string | null) => void;
toggleDeleteIssueModal: (issueId: string | null) => void;
- toggleArchiveIssueModal: (value: boolean) => void;
- toggleRelationModal: (relationType: TIssueRelationTypes | null) => void;
- toggleSubIssuesModal: (value: boolean) => void;
+ toggleArchiveIssueModal: (value: string | null) => void;
+ toggleRelationModal: (issueId: string | null, relationType: TIssueRelationTypes | null) => void;
+ toggleSubIssuesModal: (value: string | null) => void;
toggleDeleteAttachmentModal: (attachmentId: string | null) => void;
// store
rootIssueStore: IIssueRootStore;
@@ -85,11 +90,11 @@ export class IssueDetail implements IIssueDetail {
peekIssue: TPeekIssue | undefined = undefined;
isCreateIssueModalOpen: boolean = false;
isIssueLinkModalOpen: boolean = false;
- isParentIssueModalOpen: boolean = false;
+ isParentIssueModalOpen: string | null = null;
isDeleteIssueModalOpen: string | null = null;
- isArchiveIssueModalOpen: boolean = false;
- isRelationModalOpen: TIssueRelationTypes | null = null;
- isSubIssuesModalOpen: boolean = false;
+ isArchiveIssueModalOpen: string | null = null;
+ isRelationModalOpen: TIssueRelationModal | null = null;
+ isSubIssuesModalOpen: string | null = null;
isDeleteAttachmentModalOpen: string | null = null;
// store
rootIssueStore: IIssueRootStore;
@@ -149,11 +154,11 @@ export class IssueDetail implements IIssueDetail {
return (
this.isCreateIssueModalOpen ||
this.isIssueLinkModalOpen ||
- this.isParentIssueModalOpen ||
+ !!this.isParentIssueModalOpen ||
!!this.isDeleteIssueModalOpen ||
- this.isArchiveIssueModalOpen ||
- !!this.isRelationModalOpen ||
- this.isSubIssuesModalOpen ||
+ !!this.isArchiveIssueModalOpen ||
+ !!this.isRelationModalOpen?.issueId ||
+ !!this.isSubIssuesModalOpen ||
!!this.isDeleteAttachmentModalOpen
);
}
@@ -165,11 +170,12 @@ export class IssueDetail implements IIssueDetail {
setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue);
toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value);
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
- toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
+ toggleParentIssueModal = (issueId: string | null) => (this.isParentIssueModalOpen = issueId);
toggleDeleteIssueModal = (issueId: string | null) => (this.isDeleteIssueModalOpen = issueId);
- toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value);
- toggleRelationModal = (relationType: TIssueRelationTypes | null) => (this.isRelationModalOpen = relationType);
- toggleSubIssuesModal = (value: boolean) => (this.isSubIssuesModalOpen = value);
+ toggleArchiveIssueModal = (issueId: string | null) => (this.isArchiveIssueModalOpen = issueId);
+ toggleRelationModal = (issueId: string | null, relationType: TIssueRelationTypes | null) =>
+ (this.isRelationModalOpen = { issueId, relationType });
+ toggleSubIssuesModal = (issueId: string | null) => (this.isSubIssuesModalOpen = issueId);
toggleDeleteAttachmentModal = (attachmentId: string | null) => (this.isDeleteAttachmentModalOpen = attachmentId);
// issue
diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts
index b6611ef494..b2b33464c2 100644
--- a/web/store/issue/issue.store.ts
+++ b/web/store/issue/issue.store.ts
@@ -6,6 +6,7 @@ import { computedFn } from "mobx-utils";
// types
import { IssueService } from "@/services/issue";
import { TIssue } from "@plane/types";
+import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper";
//services
export type IIssueStore = {
@@ -76,6 +77,7 @@ export class IssueStore implements IIssueStore {
updateIssue = (issueId: string, issue: Partial) => {
if (!issue || !issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return;
runInAction(() => {
+ set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
Object.keys(issue).forEach((key) => {
set(this.issuesMap, [issueId, key], issue[key as keyof TIssue]);
});
diff --git a/web/styles/globals.css b/web/styles/globals.css
index 1c4d038544..c3f7a0baf2 100644
--- a/web/styles/globals.css
+++ b/web/styles/globals.css
@@ -632,3 +632,8 @@ div.web-view-spinner div.bar12 {
.scrollbar-lg::-webkit-scrollbar-thumb {
border: 4px solid rgba(0, 0, 0, 0);
}
+
+/* highlight class */
+.highlight {
+ border: 1px solid rgb(var(--color-primary-100)) !important;
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index a33c24f885..268c041eba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3699,6 +3699,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
+compute-scroll-into-view@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
+ integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -7633,6 +7638,13 @@ schema-utils@^3.1.1:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
+scroll-into-view-if-needed@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f"
+ integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==
+ dependencies:
+ compute-scroll-into-view "^3.0.2"
+
selecto@~1.26.3:
version "1.26.3"
resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212"
@@ -7774,6 +7786,13 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+smooth-scroll-into-view-if-needed@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-2.0.2.tgz#5bd4ebef668474d6618ce8704650082e93068371"
+ integrity sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q==
+ dependencies:
+ scroll-into-view-if-needed "^3.1.0"
+
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"