mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.18.0"
|
||||
"version": "0.19.0"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class Command(BaseCommand):
|
||||
}
|
||||
|
||||
instance = Instance.objects.create(
|
||||
instance_name="Plane Free",
|
||||
instance_name="Plane Community Edition",
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/document-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Pages Editor",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-extensions",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Editor with extensions",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/lite-text-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Package that powers Plane's Comment Editor",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/rich-text-editor",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "Rich Text Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
||||
@@ -51,7 +51,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
suggestions: mentionSuggestions,
|
||||
}}
|
||||
{...rest}
|
||||
containerClassName={cn("relative min-h-[150px] pl-3", containerClassName)}
|
||||
containerClassName={cn("relative pl-3", containerClassName)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
||||
}}
|
||||
{...props}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(props.containerClassName, "relative border border-custom-border-200 p-3")}
|
||||
containerClassName={cn(props.containerClassName, "relative pl-3")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("relative flex flex-col h-full select-none rounded-sm bg-custom-background-100 shadow", {
|
||||
"fixed inset-0 z-[999999] bg-custom-background-100": fullScreenMode,
|
||||
"fixed inset-0 z-20 bg-custom-background-100": fullScreenMode,
|
||||
"border-[0.5px] border-custom-border-200": border,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -292,32 +292,36 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
</ControlLink>
|
||||
</div>
|
||||
) : (
|
||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||
{canMarkAsAccepted && (
|
||||
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
Snooze
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<>
|
||||
{isAllowed && (
|
||||
<CustomMenu verticalEllipsis placement="bottom-start">
|
||||
{canMarkAsAccepted && (
|
||||
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} strokeWidth={2} />
|
||||
Snooze
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canMarkAsDuplicate && (
|
||||
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileStack size={14} strokeWidth={2} />
|
||||
Mark as duplicate
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canDelete && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
{canMarkAsDuplicate && (
|
||||
<CustomMenu.MenuItem onClick={() => setSelectDuplicateIssue(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileStack size={14} strokeWidth={2} />
|
||||
Mark as duplicate
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{canDelete && (
|
||||
<CustomMenu.MenuItem onClick={() => setDeleteIssueModal(true)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3"
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} />
|
||||
<div className="relative flex justify-between items-center gap-3">
|
||||
|
||||
@@ -138,7 +138,7 @@ export const InboxIssueEditRoot: FC<TInboxIssueEditRoot> = observer((props) => {
|
||||
data={formData}
|
||||
handleData={handleFormData}
|
||||
editorRef={descriptionEditorRef}
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3"
|
||||
containerClassName="border-[0.5px] border-custom-border-200 py-3 min-h-[150px]"
|
||||
/>
|
||||
<InboxIssueProperties projectId={projectId} data={formData} handleData={handleFormData} isVisible />
|
||||
<div className="relative flex justify-end items-center gap-3">
|
||||
|
||||
@@ -22,7 +22,7 @@ type TInboxIssueDescription = {
|
||||
|
||||
// TODO: have to implement GPT Assistance
|
||||
export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props) => {
|
||||
const {containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
const { containerClassName, workspaceSlug, projectId, workspaceId, data, handleData, editorRef } = props;
|
||||
// hooks
|
||||
const { loader } = useProjectInbox();
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||
disabled={!isEditable}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
{/* )} */}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
try {
|
||||
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId });
|
||||
await issueOperations.fetch(workspaceSlug, projectId, issueId);
|
||||
toggleParentIssueModal(false);
|
||||
toggleParentIssueModal(issueId);
|
||||
} catch (error) {
|
||||
console.error("something went wrong while fetching the issue");
|
||||
}
|
||||
@@ -79,8 +79,8 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
<ParentIssuesListModal
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
isOpen={isParentIssueModalOpen}
|
||||
handleClose={() => toggleParentIssueModal(false)}
|
||||
isOpen={isParentIssueModalOpen === issueId}
|
||||
handleClose={() => toggleParentIssueModal(null)}
|
||||
onChange={(issue: any) => handleParentIssue(issue?.id)}
|
||||
/>
|
||||
<button
|
||||
@@ -94,7 +94,7 @@ export const IssueParentSelect: React.FC<TIssueParentSelect> = observer((props)
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={() => toggleParentIssueModal(true)}
|
||||
onClick={() => toggleParentIssueModal(issue.id)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{issue.parent_id && parentIssue ? (
|
||||
|
||||
@@ -81,22 +81,26 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
data.map((i) => i.id)
|
||||
);
|
||||
|
||||
toggleRelationModal(null);
|
||||
toggleRelationModal(null, null);
|
||||
};
|
||||
|
||||
if (!relationIssueIds) return null;
|
||||
|
||||
const isRelationKeyModalActive =
|
||||
isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
isOpen={isRelationModalOpen === relationKey}
|
||||
handleClose={() => toggleRelationModal(null)}
|
||||
isOpen={isRelationKeyModalActive}
|
||||
handleClose={() => toggleRelationModal(null, null)}
|
||||
searchParams={{ issue_relation: true, issue_id: issueId }}
|
||||
handleOnSubmit={onSubmit}
|
||||
workspaceLevelToggle
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
@@ -104,11 +108,11 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
||||
{
|
||||
"cursor-not-allowed": disabled,
|
||||
"hover:bg-custom-background-80": !disabled,
|
||||
"bg-custom-background-80": isRelationModalOpen === relationKey,
|
||||
"bg-custom-background-80": isRelationKeyModalActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
onClick={() => toggleRelationModal(relationKey)}
|
||||
onClick={() => toggleRelationModal(issueId, relationKey)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex w-full items-start justify-between">
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = 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<IBaseKanBanLayout> = 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<IBaseKanBanLayout> = observer((props: IBas
|
||||
displayProperties={displayProperties}
|
||||
sub_group_by={sub_group_by}
|
||||
group_by={group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={renderQuickActions}
|
||||
handleKanbanFilters={handleKanbanFilters}
|
||||
|
||||
@@ -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<IssueBlockProps> = 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<IssueBlockProps> = observer((props) => {
|
||||
<div
|
||||
// make Z-index higher at the beginning of drag, to have a issue drag image of issue block without any overlaps
|
||||
className={cn("group/kanban-block relative p-1.5", { "z-[1]": isCurrentBlockDragging })}
|
||||
onDragStart={() => 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",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ControlLink
|
||||
id={`issue-${issue.id}`}
|
||||
@@ -186,12 +199,10 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = 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)}
|
||||
|
||||
@@ -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<TIssue>) => Promise<void>) | undefined;
|
||||
@@ -79,6 +81,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleOnDrop,
|
||||
showEmptyGroup = true,
|
||||
subGroupIssueHeaderCount,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const member = useMember();
|
||||
@@ -170,6 +173,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = 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<TIssue>) => Promise<void>) | undefined;
|
||||
quickActions: TRenderQuickActions;
|
||||
@@ -242,6 +247,7 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
handleOnDrop,
|
||||
showEmptyGroup,
|
||||
subGroupIssueHeaderCount,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const issueKanBanView = useKanbanView();
|
||||
@@ -253,6 +259,7 @@ export const KanBan: React.FC<IKanBan> = 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}
|
||||
|
||||
@@ -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<HTMLDivElement | null>;
|
||||
handleOnDrop: (source: KanbanDropLocation, destination: KanbanDropLocation) => Promise<void>;
|
||||
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 (
|
||||
<div
|
||||
id={`${groupId}__${sub_group_id}`}
|
||||
className={cn(
|
||||
"relative h-full transition-all min-h-[50px]",
|
||||
{ "bg-custom-background-80": isDraggingOverColumn },
|
||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by }
|
||||
{ "bg-custom-background-80 rounded": isDraggingOverColumn },
|
||||
{ "vertical-scrollbar scrollbar-md": !sub_group_by && !shouldOverlay }
|
||||
)}
|
||||
ref={columnRef}
|
||||
>
|
||||
<div
|
||||
//column overlay when issues are not sorted by manual
|
||||
className={cn(
|
||||
"absolute top-0 left-0 h-full w-full items-center text-sm font-medium text-custom-text-300 rounded",
|
||||
{
|
||||
"flex flex-col bg-custom-background-80 border-[1px] border-custom-border-300 z-[2]": shouldOverlay,
|
||||
},
|
||||
{ hidden: !shouldOverlay },
|
||||
{ "justify-center": !sub_group_by }
|
||||
)}
|
||||
>
|
||||
{readableOrderBy && <span className="pt-6">The layout is ordered by {readableOrderBy}.</span>}
|
||||
<span>Drop here to move the issue.</span>
|
||||
</div>
|
||||
<KanbanIssueBlocksList
|
||||
sub_group_id={sub_group_id}
|
||||
columnId={groupId}
|
||||
@@ -181,7 +209,7 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
canEditProperties={canEditProperties}
|
||||
scrollableContainerRef={scrollableContainerRef}
|
||||
scrollableContainerRef={sub_group_by ? scrollableContainerRef : columnRef}
|
||||
/>
|
||||
|
||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||
|
||||
@@ -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<TIssue>;
|
||||
quickAddCallback?: (
|
||||
@@ -146,6 +148,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
viewId,
|
||||
scrollableContainerRef,
|
||||
handleOnDrop,
|
||||
orderBy,
|
||||
} = props;
|
||||
|
||||
const calculateIssueCount = (column_id: string) => {
|
||||
@@ -181,7 +184,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
if (subGroupByVisibilityToggle.showGroup === false) return <></>;
|
||||
return (
|
||||
<div key={_list.id} className="flex flex-shrink-0 flex-col">
|
||||
<div className="sticky top-[50px] z-[1] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||
<div className="sticky top-[50px] z-[3] py-1 flex w-full items-center bg-custom-background-100 border-y-[0.5px] border-custom-border-200">
|
||||
<div className="sticky left-0 flex-shrink-0">
|
||||
<HeaderSubGroupByCard
|
||||
column_id={_list.id}
|
||||
@@ -216,6 +219,7 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = 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<HTMLDivElement | null>;
|
||||
orderBy: TIssueOrderByOptions | undefined;
|
||||
}
|
||||
|
||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
@@ -263,6 +268,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
displayProperties,
|
||||
sub_group_by,
|
||||
group_by,
|
||||
orderBy,
|
||||
updateIssue,
|
||||
storeType,
|
||||
quickActions,
|
||||
@@ -313,7 +319,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="sticky top-0 z-[2] h-[50px] bg-custom-background-90 px-2">
|
||||
<div className="sticky top-0 z-[4] h-[50px] bg-custom-background-90 px-2">
|
||||
<SubGroupSwimlaneHeader
|
||||
issueIds={issueIds}
|
||||
group_by={group_by}
|
||||
@@ -334,6 +340,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||
displayProperties={displayProperties}
|
||||
group_by={group_by}
|
||||
sub_group_by={sub_group_by}
|
||||
orderBy={orderBy}
|
||||
updateIssue={updateIssue}
|
||||
quickActions={quickActions}
|
||||
kanbanFilters={kanbanFilters}
|
||||
|
||||
@@ -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<TIssue>) => Promise<void>) | 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);
|
||||
};
|
||||
|
||||
@@ -480,7 +480,7 @@ export const IssueFormRoot: FC<IssueFormProps> = 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]"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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<PeekOverviewHeaderProps> = observer((pr
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isInArchivableGroup) return;
|
||||
toggleArchiveIssueModal(true);
|
||||
toggleArchiveIssueModal(issueId);
|
||||
}}
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
|
||||
@@ -82,7 +82,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = observer(
|
||||
disabled={disabled}
|
||||
issueOperations={issueOperations}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
containerClassName="-ml-3 border-none"
|
||||
containerClassName="-ml-3 !mb-6 border-none"
|
||||
/>
|
||||
|
||||
{currentUser && (
|
||||
|
||||
@@ -87,8 +87,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
<>
|
||||
{issue && !is_archived && (
|
||||
<ArchiveIssueModal
|
||||
isOpen={isArchiveIssueModalOpen}
|
||||
handleClose={() => toggleArchiveIssueModal(false)}
|
||||
isOpen={isArchiveIssueModalOpen === issueId}
|
||||
handleClose={() => toggleArchiveIssueModal(null)}
|
||||
data={issue}
|
||||
onSubmit={async () => {
|
||||
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
|
||||
|
||||
@@ -381,7 +381,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -439,7 +439,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = observer((props) => {
|
||||
onClick={() => {
|
||||
setTrackElement("Issue detail nested sub-issue");
|
||||
handleIssueCrudState("existing", parentIssueId, null);
|
||||
toggleSubIssuesModal(true);
|
||||
toggleSubIssuesModal(issue.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -476,7 +476,7 @@ export const SubIssuesRoot: FC<ISubIssuesRoot> = 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) =>
|
||||
|
||||
@@ -84,17 +84,19 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = 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, {
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -9,23 +9,25 @@ type TUseDropdownKeyDown = {
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
|
||||
const stopEventPropagation = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (stopPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
const stopEventPropagation = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (stopPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[stopPropagation]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
stopEventPropagation(event);
|
||||
|
||||
onEnterKeyDown();
|
||||
} else if (event.key === "Escape") {
|
||||
stopEventPropagation(event);
|
||||
onEscKeyDown();
|
||||
}
|
||||
} else if (event.key === "Tab") onEscKeyDown();
|
||||
},
|
||||
[onEnterKeyDown, onEscKeyDown, stopEventPropagation]
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<IWorkspaceView>;
|
||||
// crud actions
|
||||
createGlobalView: (workspaceSlug: string, data: Partial<IWorkspaceView>) => Promise<IWorkspaceView>;
|
||||
updateGlobalView: (workspaceSlug: string, viewId: string, data: Partial<IWorkspaceView>) => Promise<IWorkspaceView>;
|
||||
updateGlobalView: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
) => Promise<IWorkspaceView | undefined>;
|
||||
deleteGlobalView: (workspaceSlug: string, viewId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
@@ -139,14 +148,50 @@ export class GlobalViewStore implements IGlobalViewStore {
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
): Promise<IWorkspaceView> =>
|
||||
await this.workspaceService.updateView(workspaceSlug, viewId, data).then((response) => {
|
||||
const viewToUpdate = { ...this.getViewDetailsById(viewId), ...data };
|
||||
runInAction(() => {
|
||||
set(this.globalViewMap, viewId, viewToUpdate);
|
||||
): Promise<IWorkspaceView | undefined> => {
|
||||
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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<TIssue>) => {
|
||||
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]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
19
yarn.lock
19
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"
|
||||
|
||||
Reference in New Issue
Block a user