Skip to content

Commit

Permalink
feat: quick add (#2240)
Browse files Browse the repository at this point in the history
* feat: quick add

* style: made text color muted
  • Loading branch information
dakshesh14 authored Sep 22, 2023
1 parent daa0b16 commit 771ca58
Show file tree
Hide file tree
Showing 17 changed files with 847 additions and 99 deletions.
1 change: 1 addition & 0 deletions web/components/core/views/board-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./all-boards";
export * from "./board-header";
export * from "./single-board";
export * from "./single-issue";
export * from "./inline-create-issue-form";
62 changes: 62 additions & 0 deletions web/components/core/views/board-view/inline-create-issue-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect } from "react";

// react hook form
import { useFormContext } from "react-hook-form";

// components
import { InlineCreateIssueFormWrapper } from "components/core";

// hooks
import useProjectDetails from "hooks/use-project-details";

// types
import { IIssue } from "types";

type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};

const InlineInput = () => {
const { projectDetails } = useProjectDetails();

const { register, setFocus } = useFormContext();

useEffect(() => {
setFocus("name");
}, [setFocus]);

return (
<div>
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
{projectDetails?.identifier ?? "..."}
</h4>
<input
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</div>
);
};

export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
<>
<InlineCreateIssueFormWrapper
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow"
{...props}
>
<InlineInput />
</InlineCreateIssueFormWrapper>
{props.isOpen && (
<p className="text-xs ml-3 italic text-custom-text-200">
Press {"'"}Enter{"'"} to add another issue
</p>
)}
</>
);
76 changes: 56 additions & 20 deletions web/components/core/views/board-view/single-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useRouter } from "next/router";
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { Draggable } from "react-beautiful-dnd";
// components
import { BoardHeader, SingleBoardIssue } from "components/core";
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
// ui
import { CustomMenu } from "components/ui";
// icons
Expand Down Expand Up @@ -34,26 +34,30 @@ type Props = {
viewProps: IIssueViewProps;
};

export const SingleBoard: React.FC<Props> = ({
addIssueToGroup,
currentState,
groupTitle,
disableUserActions,
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
handleMyIssueOpen,
removeIssue,
user,
userAuth,
viewProps,
}) => {
export const SingleBoard: React.FC<Props> = (props) => {
const {
addIssueToGroup,
currentState,
groupTitle,
disableUserActions,
disableAddIssueOption = false,
dragDisabled,
handleIssueAction,
handleDraftIssueAction,
handleTrashBox,
openIssuesListModal,
handleMyIssueOpen,
removeIssue,
user,
userAuth,
viewProps,
} = props;

// collapse/expand
const [isCollapsed, setIsCollapsed] = useState(true);

const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);

const { displayFilters, groupedIssues } = viewProps;

const router = useRouter();
Expand All @@ -67,6 +71,24 @@ export const SingleBoard: React.FC<Props> = ({

const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;

const onCreateClick = () => {
setIsInlineCreateIssueFormOpen(true);

const boardListElement = document.getElementById(`board-list-${groupTitle}`);

// timeout is needed because the animation
// takes time to complete & we can scroll only after that
const timeoutId = setTimeout(() => {
if (boardListElement)
boardListElement.scrollBy({
top: boardListElement.scrollHeight,
left: 0,
behavior: "smooth",
});
clearTimeout(timeoutId);
}, 10);
};

return (
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
<BoardHeader
Expand Down Expand Up @@ -115,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
</>
)}
<div
id={`board-list-${groupTitle}`}
className={`pt-3 ${
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
} `}
Expand Down Expand Up @@ -169,6 +192,19 @@ export const SingleBoard: React.FC<Props> = ({
>
<>{provided.placeholder}</>
</span>

<BoardInlineCreateIssueForm
isOpen={isInlineCreateIssueFormOpen}
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
prePopulatedData={{
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
[displayFilters?.group_by! === "labels"
? "labels_list"
: displayFilters?.group_by!]:
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
}}
/>
</div>
{displayFilters?.group_by !== "created_by" && (
<div>
Expand All @@ -177,7 +213,7 @@ export const SingleBoard: React.FC<Props> = ({
<button
type="button"
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
onClick={addIssueToGroup}
onClick={() => onCreateClick()}
>
<PlusIcon className="h-4 w-4" />
Add Issue
Expand All @@ -197,7 +233,7 @@ export const SingleBoard: React.FC<Props> = ({
position="left"
noBorder
>
<CustomMenu.MenuItem onClick={addIssueToGroup}>
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
Create new
</CustomMenu.MenuItem>
{openIssuesListModal && (
Expand Down
1 change: 1 addition & 0 deletions web/components/core/views/calendar-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./calendar-header";
export * from "./calendar";
export * from "./single-date";
export * from "./single-issue";
export * from "./inline-create-issue-form";
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useEffect, useRef, useState } from "react";

// react hook form
import { useFormContext } from "react-hook-form";

import { InlineCreateIssueFormWrapper } from "components/core";

// hooks
import useProjectDetails from "hooks/use-project-details";

// types
import { IIssue } from "types";

type Props = {
isOpen: boolean;
handleClose: () => void;
onSuccess?: (data: IIssue) => Promise<void> | void;
prePopulatedData?: Partial<IIssue>;
};

const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);

useEffect(() => {
if (!ref.current) return;

const { right } = ref.current.getBoundingClientRect();

const width = right + 250;

if (width > window.innerWidth) setIsThereSpaceOnRight(false);
else setIsThereSpaceOnRight(true);
}, [ref]);

return isThereSpaceOnRight;
};

const InlineInput = () => {
const { projectDetails } = useProjectDetails();

const { register, setFocus } = useFormContext();

useEffect(() => {
setFocus("name");
}, [setFocus]);

return (
<>
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
{projectDetails?.identifier ?? "..."}
</h4>
<input
type="text"
autoComplete="off"
placeholder="Issue Title"
{...register("name", {
required: "Issue title is required.",
})}
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
/>
</>
);
};

export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
const { isOpen } = props;

const ref = useRef<HTMLDivElement>(null);

const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);

return (
<>
<div
ref={ref}
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
>
<InlineCreateIssueFormWrapper
{...props}
className="flex w-60 p-1 px-1.5 rounded items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-md transition-opacity"
>
<InlineInput />
</InlineCreateIssueFormWrapper>
</div>
{/* Added to make any other element as outside click. This will make input also to be outside. */}
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
</>
);
};
34 changes: 23 additions & 11 deletions web/components/core/views/calendar-view/single-date.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import React, { useState } from "react";

// next
import { useRouter } from "next/router";

// react-beautiful-dnd
import { Draggable } from "react-beautiful-dnd";
// component
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import { SingleCalendarIssue } from "./single-issue";
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
// icons
import { PlusSmallIcon } from "@heroicons/react/24/outline";
// helper
Expand All @@ -26,17 +30,14 @@ type Props = {
isNotAllowed: boolean;
};

export const SingleCalendarDate: React.FC<Props> = ({
handleIssueAction,
date,
index,
addIssueToDate,
isMonthlyView,
showWeekEnds,
user,
isNotAllowed,
}) => {
export const SingleCalendarDate: React.FC<Props> = (props) => {
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;

const router = useRouter();
const { cycleId, moduleId } = router.query;

const [showAllIssues, setShowAllIssues] = useState(false);
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);

const totalIssues = date.issues.length;

Expand Down Expand Up @@ -78,6 +79,17 @@ export const SingleCalendarDate: React.FC<Props> = ({
)}
</Draggable>
))}

<CalendarInlineCreateIssueForm
isOpen={isCreateIssueFormOpen}
handleClose={() => setIsCreateIssueFormOpen(false)}
prePopulatedData={{
target_date: date.date,
...(cycleId && { cycle: cycleId.toString() }),
...(moduleId && { module: moduleId.toString() }),
}}
/>

{totalIssues > 4 && (
<button
type="button"
Expand All @@ -93,7 +105,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
>
<button
className="flex items-center justify-center gap-1 text-center"
onClick={() => addIssueToDate(date.date)}
onClick={() => setIsCreateIssueFormOpen(true)}
>
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
Add issue
Expand Down
Loading

2 comments on commit 771ca58

@vercel
Copy link

@vercel vercel bot commented on 771ca58 Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

plane-dev – ./web/

plane-dev.vercel.app
plane-dev-git-develop-plane.vercel.app
plane-dev-plane.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 771ca58 Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

plane-sh-dev – ./space/

plane-sh-dev-plane.vercel.app
plane-sh-dev-git-develop-plane.vercel.app
plane-space-dev.vercel.app

Please sign in to comment.