style: issue peek overview ui improvement (#2574)

* style: issue peek overview ui improvement

* chore: implemented issue subscription in peek overview

* chore: issue properties dropdown refactor

* fix: build error

* chore: label select refactor

* chore: issue peekoverview revamp and refactor

* chore: issue peekoverview properties added and code refactor

---------

Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
Anmol Singh Bhatia 2023-11-01 14:22:29 +05:30 committed by GitHub
parent 10e35d9a06
commit 1be82814fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1591 additions and 850 deletions

View File

@ -130,7 +130,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
{projectDetails?.is_deployed && deployUrl && (
<a
href={`${deployUrl}/${workspaceSlug}/${projectDetails?.id}`}
className="group bg-custom-primary-100/20 text-custom-primary-100 px-2.5 py-1 text-xs flex items-center gap-1.5 rounded font-medium"
className="group bg-custom-primary-100/10 text-custom-primary-100 px-2.5 py-1 text-xs flex items-center gap-1.5 rounded font-medium"
target="_blank"
rel="noopener noreferrer"
>

View File

@ -10,4 +10,6 @@ export * from "./gantt";
export * from "./kanban";
export * from "./spreadsheet";
export * from "./properties";
export * from "./roots";

View File

@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd";
// components
import { KanBanProperties } from "./properties";
// types
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
import { IIssueDisplayProperties, IIssue } from "types";
interface IssueBlockProps {
sub_group_id: string;
@ -18,27 +18,10 @@ interface IssueBlockProps {
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
const {
sub_group_id,
columnId,
index,
issue,
isDragDisabled,
handleIssues,
quickActions,
displayProperties,
states,
labels,
members,
estimates,
} = props;
const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
@ -82,10 +65,6 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
issue={issue}
handleIssues={updateIssue}
displayProperties={displayProperties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
</div>
</div>

View File

@ -1,6 +1,6 @@
// components
import { KanbanIssueBlock } from "components/issues";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
import { IIssueDisplayProperties, IIssue } from "types";
interface IssueBlocksListProps {
sub_group_id: string;
@ -15,26 +15,10 @@ interface IssueBlocksListProps {
) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
const {
sub_group_id,
columnId,
issues,
isDragDisabled,
handleIssues,
quickActions,
displayProperties,
states,
labels,
members,
estimates,
} = props;
const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
return (
<>
@ -51,10 +35,6 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
columnId={columnId}
sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
))}
</>

View File

@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
// types
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
import { IIssueDisplayProperties, IIssue } from "types";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
@ -30,11 +30,6 @@ export interface IGroupByKanBan {
kanBanToggle: any;
handleKanBanToggle: any;
enableQuickIssueCreate?: boolean;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
priorities: any;
estimates: IEstimatePoint[] | null;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -51,11 +46,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
labels,
members,
priorities,
estimates,
enableQuickIssueCreate,
} = props;
@ -105,10 +95,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
displayProperties={displayProperties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
) : (
isDragDisabled && (
@ -154,13 +140,6 @@ export interface IKanBan {
displayProperties: IIssueDisplayProperties;
kanBanToggle: any;
handleKanBanToggle: any;
states: IState[] | null;
stateGroups: any;
priorities: any;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
estimates: IEstimatePoint[] | null;
enableQuickIssueCreate?: boolean;
}
@ -175,13 +154,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
displayProperties,
kanBanToggle,
handleKanBanToggle,
states,
stateGroups,
priorities,
labels,
members,
projects,
estimates,
enableQuickIssueCreate,
} = props;
@ -204,11 +176,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
@ -227,11 +194,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
@ -250,11 +212,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
@ -273,11 +230,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
@ -296,11 +248,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
@ -319,11 +266,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/>
)}
</div>

View File

@ -10,15 +10,7 @@ import { IssuePropertyAssignee } from "../properties/assignee";
import { IssuePropertyEstimates } from "../properties/estimates";
import { IssuePropertyDate } from "../properties/date";
import { Tooltip } from "@plane/ui";
import {
IEstimatePoint,
IIssue,
IIssueDisplayProperties,
IIssueLabels,
IState,
IUserLite,
TIssuePriorities,
} from "types";
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
export interface IKanBanProperties {
sub_group_id: string;
@ -26,24 +18,10 @@ export interface IKanBanProperties {
issue: IIssue;
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
const {
sub_group_id,
columnId: group_id,
issue,
handleIssues,
displayProperties,
states,
labels,
members,
estimates,
} = props;
const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties } = props;
const handleState = (state: IState) => {
handleIssues(
@ -107,9 +85,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{/* state */}
{displayProperties && displayProperties?.state && (
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null}
onChange={handleState}
states={states}
disabled={false}
hideDropdownArrow
/>
@ -128,9 +106,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{/* label */}
{displayProperties && displayProperties?.labels && (
<IssuePropertyLabels
projectId={issue?.project_detail?.id || null}
value={issue?.labels || null}
onChange={handleLabel}
labels={labels}
disabled={false}
hideDropdownArrow
/>
@ -139,10 +117,10 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{/* assignee */}
{displayProperties && displayProperties?.assignee && (
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
hideDropdownArrow
onChange={handleAssignee}
members={members}
disabled={false}
/>
)}
@ -170,9 +148,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
{/* estimates */}
{displayProperties && displayProperties?.estimate && (
<IssuePropertyEstimates
projectId={issue?.project_detail?.id || null}
value={issue?.estimate_point || null}
onChange={handleEstimate}
estimatePoints={estimates}
disabled={false}
hideDropdownArrow
/>

View File

@ -116,13 +116,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
/>
) : (
<KanBanSwimLanes

View File

@ -116,13 +116,6 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
/>
) : (
<KanBanSwimLanes

View File

@ -99,13 +99,6 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
estimates={null}
/>
) : (
<KanBanSwimLanes

View File

@ -106,14 +106,6 @@ export const KanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
enableQuickIssueCreate
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
/>
) : (
<KanBanSwimLanes

View File

@ -146,13 +146,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
displayProperties={displayProperties}
kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members}
projects={projects}
estimates={estimates}
enableQuickIssueCreate
/>
</div>

View File

@ -4,7 +4,7 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
import { IIssue } from "types";
interface IssueBlockProps {
columnId: string;
@ -12,14 +12,10 @@ interface IssueBlockProps {
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, estimates } = props;
const { columnId, issue, handleIssues, quickActions, display_properties } = props;
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
handleIssues(group_by, issueToUpdate, "update");
@ -54,10 +50,6 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
issue={issue}
handleIssues={updateIssue}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
</div>

View File

@ -2,7 +2,7 @@ import { FC } from "react";
// components
import { IssueBlock } from "components/issues";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
import { IIssue } from "types";
interface Props {
columnId: string;
@ -10,15 +10,10 @@ interface Props {
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const IssueBlocksList: FC<Props> = (props) => {
const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, estimates } =
props;
const { columnId, issues, handleIssues, quickActions, display_properties } = props;
return (
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
@ -31,10 +26,6 @@ export const IssueBlocksList: FC<Props> = (props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
))
) : (

View File

@ -17,14 +17,7 @@ export interface IGroupByList {
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
is_list?: boolean;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
stateGroups: any;
priorities: any;
enableQuickIssueCreate?: boolean;
estimates: IEstimatePoint[] | null;
}
const GroupByList: React.FC<IGroupByList> = observer((props) => {
@ -37,13 +30,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
quickActions,
display_properties,
is_list = false,
states,
labels,
members,
projects,
stateGroups,
priorities,
estimates,
enableQuickIssueCreate,
} = props;
@ -70,10 +56,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/>
)}
{enableQuickIssueCreate && (
@ -121,7 +103,7 @@ export const List: React.FC<IList> = observer((props) => {
projects,
stateGroups,
priorities,
estimates,
enableQuickIssueCreate,
} = props;
@ -137,13 +119,6 @@ export const List: React.FC<IList> = observer((props) => {
quickActions={quickActions}
display_properties={display_properties}
is_list
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -157,13 +132,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -177,13 +145,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -197,13 +158,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -217,13 +171,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -237,13 +184,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -257,13 +197,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}
@ -277,13 +210,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues}
quickActions={quickActions}
display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate}
/>
)}

View File

@ -11,21 +11,17 @@ import { IssuePropertyDate } from "../properties/date";
// ui
import { Tooltip } from "@plane/ui";
// types
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types";
import { IIssue, IState, TIssuePriorities } from "types";
export interface IKanBanProperties {
columnId: string;
issue: IIssue;
handleIssues: (group_by: string | null, issue: IIssue) => void;
display_properties: any;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
}
export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, estimates } = props;
const { columnId: group_id, issue, handleIssues, display_properties } = props;
const handleState = (state: IState) => {
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id });
@ -59,13 +55,13 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
<div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
{/* basic properties */}
{/* state */}
{display_properties && display_properties?.state && states && (
{display_properties && display_properties?.state && (
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null}
hideDropdownArrow
onChange={handleState}
disabled={false}
states={states}
/>
)}
@ -80,24 +76,24 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
)}
{/* label */}
{display_properties && display_properties?.labels && labels && (
{display_properties && display_properties?.labels && (
<IssuePropertyLabels
projectId={issue?.project_detail?.id || null}
value={issue?.labels || null}
onChange={handleLabel}
labels={labels}
disabled={false}
hideDropdownArrow
/>
)}
{/* assignee */}
{display_properties && display_properties?.assignee && members && (
{display_properties && display_properties?.assignee && (
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
hideDropdownArrow
onChange={handleAssignee}
disabled={false}
members={members}
/>
)}
@ -124,8 +120,8 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
{/* estimates */}
{display_properties && display_properties?.estimate && (
<IssuePropertyEstimates
projectId={issue?.project_detail?.id || null}
value={issue?.estimate_point || null}
estimatePoints={estimates}
hideDropdownArrow
onChange={handleEstimate}
disabled={false}

View File

@ -1,28 +1,180 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { MembersSelect } from "components/project";
import { useMobxStore } from "lib/mobx/store-provider";
import { usePopper } from "react-popper";
// ui
import { AssigneesList, Avatar } from "components/ui";
import { Combobox } from "@headlessui/react";
import { Tooltip } from "@plane/ui";
import { Check, ChevronDown, Search, User2 } from "lucide-react";
// types
import { IUserLite } from "types";
import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyAssignee {
value: string[];
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: string[] | string;
onChange: (data: string[]) => void;
members: IUserLite[] | null;
disabled?: boolean;
hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
multiple?: true;
}
export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer((props) => {
const { value, onChange, members, disabled = false, hideDropdownArrow = false } = props;
const {
view,
projectId,
value,
onChange,
disabled = false,
hideDropdownArrow = false,
className,
buttonClassName,
optionsClassName,
placement,
multiple = false,
} = props;
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const projectMembers = projectId && projectStore?.members?.[projectId];
const fetchProjectMembers = () =>
workspaceSlug && projectId && projectStore.fetchProjectMembers(workspaceSlug, projectId);
const options = (projectMembers ? projectMembers : [])?.map((member) => ({
value: member.member.id,
query: member.member.display_name,
content: (
<div className="flex items-center gap-2">
<Avatar user={member.member} />
{member.member.display_name}
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = (
<Tooltip
tooltipHeading="Assignee"
tooltipContent={
value && value.length > 0
? (projectMembers ? projectMembers : [])
?.filter((m) => value.includes(m.member.display_name))
.map((m) => m.member.display_name)
.join(", ")
: "No Assignee"
}
position="top"
>
<div className="flex items-center cursor-pointer h-full w-full gap-2 text-custom-text-200">
{value && value.length > 0 && Array.isArray(value) ? (
<AssigneesList userIds={value} length={3} showLength={true} />
) : (
<span
className="flex items-center justify-between gap-1 h-full w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none
"
>
<User2 className="h-3 w-3" />
</span>
)}
</div>
</Tooltip>
);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const comboboxProps: any = { value, onChange, disabled };
if (multiple) comboboxProps.multiple = true;
return (
<MembersSelect
value={value}
onChange={onChange}
members={members ?? undefined}
disabled={disabled}
hideDropdownArrow={hideDropdownArrow}
multiple
/>
<Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => fetchProjectMembers()}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active && !selected ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className={`h-3.5 w-3.5`} />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
});

View File

@ -42,7 +42,7 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
<>
<Popover.Button
ref={dropdownBtn}
className={`px-2.5 py-1 h-5 flex items-center rounded border-[0.5px] border-custom-border-300 duration-300 outline-none ${
className={`px-2.5 py-1 h-5 flex items-center rounded border-[0.5px] border-custom-border-300 duration-300 outline-none w-full ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
}`}
>

View File

@ -1,28 +1,168 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { EstimateSelect } from "components/estimates";
// hooks
import { usePopper } from "react-popper";
import useEstimateOption from "hooks/use-estimate-option";
// ui
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
import { Combobox } from "@headlessui/react";
import { Tooltip } from "@plane/ui";
// types
import { IEstimatePoint } from "types";
import { Placement } from "@popperjs/core";
export interface IIssuePropertyEstimates {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: number | null;
onChange: (value: number | null) => void;
estimatePoints: IEstimatePoint[] | null;
disabled?: boolean;
hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
}
export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observer((props) => {
const { value, onChange, estimatePoints, disabled, hideDropdownArrow = false } = props;
const {
view,
projectId,
value,
onChange,
disabled,
hideDropdownArrow = false,
className = "",
buttonClassName = "",
optionsClassName = "",
placement,
} = props;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { isEstimateActive, estimatePoints } = useEstimateOption();
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map(
(estimate) => ({
value: estimate.key,
query: estimate.value,
content: (
<div className="flex items-center gap-2">
<Triangle className="h-3 w-3" strokeWidth={2} />
{estimate.value}
</div>
),
})
);
options?.unshift({
value: null,
query: "none",
content: (
<div className="flex items-center gap-2">
<Triangle className="h-3 w-3" strokeWidth={2} />
None
</div>
),
});
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const selectedEstimate = estimatePoints?.find((e) => e.key === value);
const label = (
<Tooltip tooltipHeading="Estimate" tooltipContent={selectedEstimate?.value ?? "None"} position="top">
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
<Triangle className="h-3 w-3" strokeWidth={2} />
<span className="truncate">{selectedEstimate?.value ?? "None"}</span>
</div>
</Tooltip>
);
return (
<EstimateSelect
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value}
onChange={onChange}
estimatePoints={estimatePoints ?? undefined}
buttonClassName="h-5"
onChange={(val) => onChange(val as number | null)}
disabled={disabled}
hideDropdownArrow={hideDropdownArrow}
/>
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
});

View File

@ -0,0 +1,6 @@
export * from "./assignee";
export * from "./date";
export * from "./estimates";
export * from "./labels";
export * from "./priority";
export * from "./state";

View File

@ -1,28 +1,212 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { usePopper } from "react-popper";
// components
import { LabelSelect } from "components/labels";
import { Combobox } from "@headlessui/react";
import { Tooltip } from "@plane/ui";
import { Check, ChevronDown, Search } from "lucide-react";
// types
import { IIssueLabels } from "types";
import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyLabels {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: string[];
onChange: (data: string[]) => void;
labels: IIssueLabels[] | null;
disabled?: boolean;
hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
maxRender?: number;
}
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
const { value, onChange, labels, disabled, hideDropdownArrow = false } = props;
const {
view,
projectId,
value,
onChange,
disabled,
hideDropdownArrow = false,
className,
buttonClassName,
optionsClassName,
placement,
maxRender = 2,
} = props;
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const projectLabels = projectId && projectStore?.labels?.[projectId];
const fetchProjectLabels = () =>
workspaceSlug && projectId && projectStore.fetchProjectLabels(workspaceSlug, projectId);
const options = (projectLabels ? projectLabels : []).map((label) => ({
value: label.id,
query: label.name,
content: (
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span>{label.name}</span>
</div>
),
}));
const filteredOptions =
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
return (
<LabelSelect
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value}
onChange={onChange}
labels={labels ?? undefined}
buttonClassName="h-5"
disabled={disabled}
hideDropdownArrow={hideDropdownArrow}
/>
multiple
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs ${
disabled
? "cursor-not-allowed text-custom-text-200"
: value.length <= maxRender
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => fetchProjectLabels()}
>
<div className="flex items-center gap-2 text-custom-text-200 h-full">
{value.length > 0 ? (
value.length <= maxRender ? (
<>
{(projectLabels ? projectLabels : [])
?.filter((l) => value.includes(l.id))
.map((label) => (
<div
key={label.id}
className="flex cursor-default items-center flex-shrink-0 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs h-full"
>
<div className="flex items-center gap-1.5 text-custom-text-200">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color ?? "#000000",
}}
/>
{label.name}
</div>
</div>
))}
</>
) : (
<div className="h-full flex cursor-default items-center flex-shrink-0 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs">
<Tooltip
position="top"
tooltipHeading="Labels"
tooltipContent={(projectLabels ? projectLabels : [])
?.filter((l) => value.includes(l.id))
.map((l) => l.name)
.join(", ")}
>
<div className="h-full flex items-center gap-1.5 text-custom-text-200">
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
{`${value.length} Labels`}
</div>
</Tooltip>
</div>
)
) : (
<div className="h-full flex items-center justify-center text-xs rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 hover:bg-custom-background-80">
Select labels
</div>
)}
</div>
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options>
<div
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className={`h-3.5 w-3.5`} />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
);
});

View File

@ -1,28 +1,175 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite";
// components
import { StateSelect } from "components/states";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { usePopper } from "react-popper";
// ui
import { Combobox } from "@headlessui/react";
import { StateGroupIcon, Tooltip } from "@plane/ui";
import { Check, ChevronDown, Search } from "lucide-react";
// types
import { IState } from "types";
import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyState {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: IState;
onChange: (state: IState) => void;
states: IState[] | null;
disabled?: boolean;
hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
}
export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props) => {
const { value, onChange, states, disabled, hideDropdownArrow = false } = props;
const {
view,
projectId,
value,
onChange,
disabled,
hideDropdownArrow = false,
className,
buttonClassName,
optionsClassName,
placement,
} = props;
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
const workspaceSlug = workspaceStore?.workspaceSlug;
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const projectStates: IState[] = [];
const projectStatesByGroup = projectId && projectStore?.states?.[projectId];
if (projectStatesByGroup)
for (const group in projectStatesByGroup) projectStates.push(...projectStatesByGroup[group]);
const fetchProjectStates = () =>
workspaceSlug && projectId && projectStore.fetchProjectStates(workspaceSlug, projectId);
const dropdownOptions = projectStates?.map((state) => ({
value: state.id,
query: state.name,
content: (
<div className="flex items-center gap-2">
<StateGroupIcon stateGroup={state.group} color={state.color} />
{state.name}
</div>
),
}));
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
modifiers: [
{
name: "preventOverflow",
options: {
padding: 12,
},
},
],
});
const filteredOptions =
query === ""
? dropdownOptions
: dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
const label = (
<Tooltip tooltipHeading="State" tooltipContent={value?.name ?? ""} position="top">
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />}
<span className="truncate">{value?.name ?? "State"}</span>
</div>
</Tooltip>
);
return (
<StateSelect
value={value}
onChange={onChange}
states={states ?? undefined}
buttonClassName="h-5"
disabled={disabled}
hideDropdownArrow={hideDropdownArrow}
/>
<>
{workspaceSlug && projectId && (
<Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value.id}
onChange={(data: string) => {
const selectedState = projectStates?.find((state) => state.id === data);
if (selectedState) onChange(selectedState);
}}
disabled={disabled}
>
<Combobox.Button as={Fragment}>
<button
ref={setReferenceElement}
type="button"
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none ${
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => fetchProjectStates()}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
</button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<div
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
<Search className="h-3.5 w-3.5 text-custom-text-300" />
<Combobox.Input
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
/>
</div>
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ active, selected }) =>
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active ? "bg-custom-background-80" : ""
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && <Check className="h-3.5 w-3.5" />}
</>
)}
</Combobox.Option>
))
) : (
<span className="flex items-center gap-2 p-1">
<p className="text-left text-custom-text-200 ">No matching results</p>
</span>
)
) : (
<p className="text-center text-custom-text-200">Loading...</p>
)}
</div>
</div>
</Combobox.Options>
</Combobox>
)}
</>
);
});

View File

@ -1,7 +1,7 @@
import React from "react";
// components
import { MembersSelect } from "components/project";
import { IssuePropertyAssignee } from "../../properties";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@ -21,11 +21,11 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return (
<div className="flex items-center h-full px-4">
<MembersSelect
<>
<IssuePropertyAssignee
projectId={issue.project_detail.id ?? null}
value={issue.assignees}
onChange={(data) => onChange({ assignees: data })}
members={members ?? []}
buttonClassName="!p-0 !rounded-none !border-0"
hideDropdownArrow
disabled={disabled}
@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
disabled={disabled}
/>
))}
</div>
</>
);
};

View File

@ -1,5 +1,5 @@
// components
import { EstimateSelect } from "components/estimates";
import { IssuePropertyEstimates } from "../../properties";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@ -21,14 +21,12 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
return (
<>
<EstimateSelect
<IssuePropertyEstimates
projectId={issue.project_detail.id ?? null}
value={issue.estimate_point}
onChange={(data) => onChange({ estimate_point: data })}
className="h-full"
buttonClassName="!border-0 !h-full !w-full !rounded-none px-4"
estimatePoints={undefined}
disabled={disabled}
hideDropdownArrow
disabled={disabled}
/>
{isExpanded &&

View File

@ -1,7 +1,7 @@
import React from "react";
// components
import { LabelSelect } from "components/labels";
import { IssuePropertyLabels } from "../../properties";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// types
@ -24,10 +24,10 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
return (
<>
<LabelSelect
<IssuePropertyLabels
projectId={issue.project_detail.id ?? null}
value={issue.labels}
onChange={(data) => onChange({ labels: data })}
labels={labels ?? []}
className="h-full"
buttonClassName="!border-0 !h-full !w-full !rounded-none"
hideDropdownArrow

View File

@ -1,11 +1,9 @@
import React from "react";
// components
import { StateSelect } from "components/states";
import { IssuePropertyState } from "../../properties";
// hooks
import useSubIssue from "hooks/use-sub-issue";
// helpers
import { getStatesList } from "helpers/state.helper";
// types
import { IIssue, IStateResponse } from "types";
@ -24,16 +22,13 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
const statesList = getStatesList(states);
return (
<>
<StateSelect
<IssuePropertyState
projectId={issue.project_detail.id ?? null}
value={issue.state_detail}
onChange={(data) => onChange({ state: data.id, state_detail: data })}
states={statesList}
className="h-full"
buttonClassName="!border-0 !h-full !w-full !rounded-none"
buttonClassName="!shadow-none !border-0"
hideDropdownArrow
disabled={disabled}
/>

View File

@ -83,7 +83,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
ref={containerRef}
className="flex max-h-full h-full overflow-y-auto divide-x-[0.5px] divide-custom-border-200"
>
{issues ? (
{issues && issues.length > 0 ? (
<>
<div className="sticky left-0 w-[28rem] z-[2]">
<div

View File

@ -34,8 +34,6 @@ export const IssueActivityCard: FC<IssueActivityCard> = (props) => {
issueCommentReactionRemove,
} = props;
console.log("issueComments", issueComments);
return (
<div className="flow-root">
<ul role="list" className="-mb-4">

View File

@ -36,8 +36,8 @@ export const IssueComment: FC<IIssueComment> = (props) => {
};
return (
<div className="space-y-4">
<div className="font-medium text-xl">Activity</div>
<div className="flex flex-col gap-3 border-t py-6 border-custom-border-200">
<div className="font-medium text-lg">Activity</div>
<div className="space-y-2">
<IssueCommentEditor

View File

@ -1,16 +1,17 @@
import { FC, useCallback, useEffect, useState } from "react";
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// packages
import { RichTextEditor } from "@plane/rich-text-editor";
// components
import { TextArea } from "@plane/ui";
import { IssueReaction } from "./reactions";
// hooks
import { useDebouncedCallback } from "use-debounce";
import useReloadConfirmations from "hooks/use-reload-confirmation";
// types
import { IIssue } from "types";
// services
import { FileService } from "services/file.service";
import { useForm, Controller } from "react-hook-form";
import useReloadConfirmations from "hooks/use-reload-confirmation";
const fileService = new FileService();
@ -26,24 +27,45 @@ interface IPeekOverviewIssueDetails {
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const { handleSubmit, watch, reset, control } = useForm<IIssue>({
const [characterLimit, setCharacterLimit] = useState(false);
const { setShowAlert } = useReloadConfirmations();
const {
handleSubmit,
watch,
reset,
control,
formState: { errors },
} = useForm<IIssue>({
defaultValues: {
name: "",
description_html: "",
},
});
const { setShowAlert } = useReloadConfirmations();
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
useEffect(() => {
if (!issue) return;
await issueUpdate({
...issue,
name: formData.name ?? "",
description_html: formData.description_html ?? "<p></p>",
});
},
[issue, issueUpdate]
);
reset({
...issue,
});
}, [issue, reset]);
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
issueUpdate({ ...issue, description_html: _data });
}, 1500);
const debouncedTitleSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500);
useEffect(() => {
if (isSubmitting === "submitted") {
@ -56,62 +78,80 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
}
}, [isSubmitting, setShowAlert]);
const handleDescriptionFormSubmit = useCallback(
async (formData: Partial<IIssue>) => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
// reset form values
useEffect(() => {
if (!issue) return;
issueUpdate({ name: formData.name ?? "", description_html: formData.description_html });
},
[issueUpdate]
);
const debouncedIssueFormSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500);
reset({
...issue,
});
}, [issue, reset]);
return (
<div className="space-y-4">
<div className="font-medium text-sm text-custom-text-200">
<>
<span className="font-medium text-base text-custom-text-400">
{issue?.project_detail?.identifier}-{issue?.sequence_id}
</div>
</span>
<div className="font-medium text-xl">{watch("name")}</div>
<div className="space-y-2">
<div className="relative">
<div className="relative">
{true ? (
<Controller
name="description_html"
name="name"
control={control}
render={({ field: { value, onChange } }) => (
<RichTextEditor
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
<TextArea
id="name"
name="name"
value={value}
onChange={(description: Object, description_html: string) => {
placeholder="Enter issue name"
onFocus={() => setCharacterLimit(true)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setCharacterLimit(false);
setIsSubmitting("submitting");
onChange(description_html);
debouncedIssueFormSave();
debouncedTitleSave();
onChange(e.target.value);
}}
customClassName="p-3 min-h-[80px] shadow-sm"
required={true}
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary !p-0 focus:!px-3 focus:!py-2"
hasError={Boolean(errors?.description)}
role="textbox"
disabled={!true}
/>
)}
/>
<div
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`}
>
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
) : (
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
)}
{characterLimit && true && (
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
{watch("name").length}
</span>
/255
</div>
</div>
<IssueReaction
issueReactions={issueReactions}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
)}
</div>
</div>
<span>{errors.name ? errors.name.message : null}</span>
<span className="text-black">
<RichTextEditor
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
deleteFile={fileService.deleteImage}
value={issue?.description_html}
debouncedUpdatesEnabled={false}
onChange={(description: Object, description_html: string) => {
debouncedIssueDescription(description_html);
}}
customClassName="mt-0"
/>
</span>
<IssueReaction
issueReactions={issueReactions}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
</>
);
};

View File

@ -1,139 +1,358 @@
import { FC } from "react";
import { FC, useState } from "react";
import { mutate } from "swr";
import { useRouter } from "next/router";
// ui icons
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { CalendarDays, Signal } from "lucide-react";
import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { CalendarDays, ContrastIcon, Link2, Plus, Signal, Tag, Triangle, User2 } from "lucide-react";
import {
SidebarAssigneeSelect,
SidebarCycleSelect,
SidebarEstimateSelect,
SidebarLabelSelect,
SidebarModuleSelect,
SidebarParentSelect,
SidebarPrioritySelect,
SidebarStateSelect,
} from "../sidebar-select";
// hooks
import useToast from "hooks/use-toast";
// components
import { IssuePropertyState } from "components/issues/issue-layouts/properties/state";
import { IssuePropertyPriority } from "components/issues/issue-layouts/properties/priority";
import { IssuePropertyAssignee } from "components/issues/issue-layouts/properties/assignee";
import { IssuePropertyDate } from "components/issues/issue-layouts/properties/date";
import { CustomDatePicker } from "components/ui";
import { LinkModal, LinksList } from "components/core";
// types
import { IIssue, IState, IUserLite, TIssuePriorities } from "types";
import { ICycle, IIssue, IIssueLink, IModule, TIssuePriorities, linkDetails } from "types";
// contexts
import { useProjectMyMembership } from "contexts/project-member.context";
import { ISSUE_DETAILS } from "constants/fetch-keys";
// services
import { IssueService } from "services/issue";
interface IPeekOverviewProperties {
issue: IIssue;
issueUpdate: (issue: Partial<IIssue>) => void;
states: IState[] | null;
members: IUserLite[] | null;
priorities: any;
user: any;
}
const issueService = new IssueService();
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = (props) => {
const { issue, issueUpdate, states, members, priorities } = props;
const { issue, issueUpdate, user } = props;
const [linkModal, setLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
const handleState = (_state: IState) => {
issueUpdate({ ...issue, state: _state.id });
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { memberRole } = useProjectMyMembership();
const handleState = (_state: string) => {
issueUpdate({ ...issue, state: _state });
};
const handlePriority = (_priority: TIssuePriorities) => {
issueUpdate({ ...issue, priority: _priority });
};
const handleAssignee = (_assignees: string[]) => {
issueUpdate({ ...issue, assignees: _assignees });
};
const handleStartDate = (_startDate: string) => {
const handleEstimate = (_estimate: number | null) => {
issueUpdate({ ...issue, estimate_point: _estimate });
};
const handleStartDate = (_startDate: string | null) => {
issueUpdate({ ...issue, start_date: _startDate });
};
const handleTargetDate = (_targetDate: string) => {
const handleTargetDate = (_targetDate: string | null) => {
issueUpdate({ ...issue, target_date: _targetDate });
};
const handleParent = (_parent: string) => {
issueUpdate({ ...issue, parent: _parent });
};
const handleCycle = (_cycle: ICycle) => {
issueUpdate({ ...issue, cycle: _cycle.id });
};
const handleModule = (_module: IModule) => {
issueUpdate({ ...issue, module: _module.id });
};
const handleLabels = (formData: Partial<IIssue>) => {
issueUpdate({ ...issue, ...formData });
};
const handleCreateLink = async (formData: IIssueLink) => {
if (!workspaceSlug || !projectId || !issue) return;
const payload = { metadata: {}, ...formData };
await issueService
.createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload)
.then(() => mutate(ISSUE_DETAILS(issue.id)))
.catch((err) => {
if (err.status === 400)
setToastAlert({
type: "error",
title: "Error!",
message: "This URL already exists for this issue.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "Something went wrong. Please try again.",
});
});
};
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
if (!workspaceSlug || !projectId || !issue) return;
const payload = { metadata: {}, ...formData };
const updatedLinks = issue.issue_link.map((l) =>
l.id === linkId
? {
...l,
title: formData.title,
url: formData.url,
}
: l
);
mutate<IIssue>(
ISSUE_DETAILS(issue.id),
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
false
);
await issueService
.updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload)
.then(() => {
mutate(ISSUE_DETAILS(issue.id));
})
.catch((err) => {
console.log(err);
});
};
const handleEditLink = (link: linkDetails) => {
setSelectedLinkToUpdate(link);
setLinkModal(true);
};
const handleDeleteLink = async (linkId: string) => {
if (!workspaceSlug || !projectId || !issue) return;
const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId);
mutate<IIssue>(
ISSUE_DETAILS(issue.id),
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
false
);
await issueService
.deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId)
.then(() => {
mutate(ISSUE_DETAILS(issue.id));
})
.catch((err) => {
console.log(err);
});
};
const minDate = issue.start_date ? new Date(issue.start_date) : null;
minDate?.setDate(minDate.getDate());
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
maxDate?.setDate(maxDate.getDate());
const isNotAllowed = user?.memberRole?.isGuest || user?.memberRole?.isViewer;
return (
<div className="space-y-4">
{/* state */}
<div className="flex items-center gap-2">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
<DoubleCircleIcon className="h-3.5 w-3.5 flex-shrink-0" />
<>
<LinkModal
isOpen={linkModal}
handleClose={() => {
setLinkModal(false);
setSelectedLinkToUpdate(null);
}}
data={selectedLinkToUpdate}
status={selectedLinkToUpdate ? true : false}
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/>
<div className="flex flex-col">
<div className="flex flex-col gap-5 py-5 w-full">
{/* state */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<p>State</p>
</div>
<div>
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={isNotAllowed} />
</div>
</div>
<div className="font-medium text-custom-text-200 line-clamp-1">State</div>
</div>
<div className="w-full">
<IssuePropertyState
value={issue?.state_detail || null}
onChange={handleState}
states={states}
disabled={false}
hideDropdownArrow
/>
</div>
</div>
{/* assignees */}
<div className="flex items-center gap-2">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
<UserGroupIcon className="h-3.5 w-3.5" />
{/* assignee */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p>
</div>
<div>
<SidebarAssigneeSelect value={issue.assignees || []} onChange={handleAssignee} disabled={isNotAllowed} />
</div>
</div>
<div className="font-medium text-custom-text-200 line-clamp-1">Assignees</div>
</div>
<div className="w-full">
<IssuePropertyAssignee
value={issue?.assignees || null}
onChange={(ids: string[]) => handleAssignee(ids)}
disabled={false}
hideDropdownArrow
members={members}
/>
</div>
</div>
{/* priority */}
<div className="flex items-center gap-2">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
<Signal className="h-3.5 w-3.5" />
{/* priority */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<Signal className="h-4 w-4 flex-shrink-0" />
<p>Priority</p>
</div>
<div>
<SidebarPrioritySelect value={issue.priority || ""} onChange={handlePriority} disabled={isNotAllowed} />
</div>
</div>
<div className="font-medium text-custom-text-200 line-clamp-1">Priority</div>
</div>
<div className="w-full">
<IssuePropertyPriority
value={issue?.priority || null}
onChange={handlePriority}
disabled={false}
hideDropdownArrow
/>
</div>
</div>
{/* start_date */}
<div className="flex items-center gap-2">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
<CalendarDays className="h-3.5 w-3.5" />
{/* estimate */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<Triangle className="h-4 w-4 flex-shrink-0 " />
<p>Estimate</p>
</div>
<div>
<SidebarEstimateSelect value={issue.estimate_point} onChange={handleEstimate} disabled={isNotAllowed} />
</div>
</div>
<div className="font-medium text-custom-text-200 line-clamp-1">Start date</div>
</div>
<div className="w-full">
<IssuePropertyDate
value={issue?.start_date || null}
onChange={(date: string) => handleStartDate(date)}
disabled={false}
placeHolder={`Start date`}
/>
</div>
</div>
{/* target_date */}
<div className="flex items-center gap-2">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
<CalendarDays className="h-3.5 w-3.5" />
{/* start date */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<p>Start date</p>
</div>
<div>
<CustomDatePicker
placeholder="Start date"
value={issue.start_date}
onChange={handleStartDate}
className="bg-custom-background-80 border-none !px-2.5 !py-0.5"
maxDate={maxDate ?? undefined}
disabled={isNotAllowed}
/>
</div>
</div>
{/* due date */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<CalendarDays className="h-4 w-4 flex-shrink-0" />
<p>Due date</p>
</div>
<div>
<CustomDatePicker
placeholder="Due date"
value={issue.target_date}
onChange={handleTargetDate}
className="bg-custom-background-80 border-none !px-2.5 !py-0.5"
minDate={minDate ?? undefined}
disabled={isNotAllowed}
/>
</div>
</div>
{/* parent */}
<div className="flex items-center gap-2 w-full">
<div className="flex items-center gap-2 w-40">
<User2 className="h-4 w-4 flex-shrink-0" />
<p>Parent</p>
</div>
<div>
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={isNotAllowed} />
</div>
</div>
<div className="font-medium text-custom-text-200 line-clamp-1">Target date</div>
</div>
<div className="w-full">
<IssuePropertyDate
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)}
disabled={false}
placeHolder={`Target date`}
/>
<span className="border-t border-custom-border-200" />
<div className="flex flex-col gap-5 py-5 w-full">
<div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40">
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
<p>Cycle</p>
</div>
<div>
<SidebarCycleSelect issueDetail={issue} handleCycleChange={handleCycle} disabled={isNotAllowed} />
</div>
</div>
<div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40">
<DiceIcon className="h-4 w-4 flex-shrink-0" />
<p>Module</p>
</div>
<div>
<SidebarModuleSelect issueDetail={issue} handleModuleChange={handleModule} disabled={isNotAllowed} />
</div>
</div>
<div className="flex items-start gap-2 w-full">
<div className="flex items-center gap-2 w-40 flex-shrink-0">
<Tag className="h-4 w-4 flex-shrink-0" />
<p>Label</p>
</div>
<div className="flex flex-col gap-3 w-full">
<SidebarLabelSelect
issueDetails={issue}
labelList={issue.labels}
submitChanges={handleLabels}
isNotAllowed={isNotAllowed}
uneditable={isNotAllowed}
/>
</div>
</div>
</div>
<span className="border-t border-custom-border-200" />
<div className="flex flex-col gap-5 pt-5 w-full">
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2 w-80">
<div className="flex items-center gap-2 w-40">
<Link2 className="h-4 w-4 rotate-45 flex-shrink-0" />
<p>Links</p>
</div>
<div>
{!isNotAllowed && (
<button
type="button"
className={`flex ${
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
onClick={() => setLinkModal(true)}
disabled={false}
>
<Plus className="h-3 w-3" /> New
</button>
)}
</div>
</div>
<div className="flex flex-col gap-3">
{issue?.issue_link && issue.issue_link.length > 0 ? (
<LinksList
links={issue.issue_link}
handleDeleteLink={handleDeleteLink}
handleEditLink={handleEditLink}
userAuth={memberRole}
/>
) : null}
</div>
</div>
</div>
</div>
</div>
</>
);
};

View File

@ -26,13 +26,13 @@ export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
type="button"
onClick={() => handleReaction(reaction)}
key={reaction}
className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded ${
isUserReacted(issueReactions[reaction])
? `bg-custom-primary-100/20 hover:bg-custom-primary-100/30`
? `bg-custom-primary-100/10 hover:bg-custom-primary-100/30`
: `bg-custom-background-90 hover:bg-custom-background-100/30`
}`}
>
<span>{renderEmoji(reaction)}</span>
<span className="text-sm">{renderEmoji(reaction)}</span>
<span
className={`${
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : ``

View File

@ -23,15 +23,11 @@ export const IssueReactionSelector: FC<IIssueReactionSelector> = (props) => {
<>
<Popover.Button
className={`${
open ? "" : "bg-custom-background-90"
} group inline-flex items-center rounded-md bg-custom-background-90 focus:outline-none transition-all hover:bg-custom-background-100`}
open ? "" : "bg-custom-background-80"
} group inline-flex items-center rounded-md bg-custom-background-80 focus:outline-none transition-all hover:bg-custom-background-90`}
>
<span
className={`flex justify-center items-center rounded-md px-2 ${
size === "sm" ? "w-6 h-6" : size === "md" ? "w-7 h-7" : "w-8 h-8"
}`}
>
<SmilePlus className="text-custom-text-100 h-3.5 w-3.5" />
<span className={`flex justify-center items-center rounded px-2 py-1.5`}>
<SmilePlus className={`${size === "sm" ? "w-3 h-3" : size === "md" ? "w-3.5 h-3.5" : "w-4 h-4"}`} />
</span>
</Popover.Button>
<Transition

View File

@ -7,8 +7,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
// types
import { IIssue } from "types";
import { RootStore } from "store/root";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
interface IIssuePeekOverview {
workspaceSlug: string;
@ -21,11 +19,7 @@ interface IIssuePeekOverview {
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { workspaceSlug, projectId, issueId, handleIssue, children } = props;
const { project: projectStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
const states = projectStore?.projectStates || undefined;
const members = projectStore?.projectMembers || undefined;
const priorities = ISSUE_PRIORITIES || undefined;
const { issueDetail: issueDetailStore }: RootStore = useMobxStore();
const issueUpdate = (_data: Partial<IIssue>) => {
if (handleIssue) {
@ -55,14 +49,17 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issueCommentReactionRemove = (commentId: string, reaction: string) =>
issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction);
const issueSubscriptionCreate = () => issueDetailStore.createIssueSubscription(workspaceSlug, projectId, issueId);
const issueSubscriptionRemove = () => issueDetailStore.removeIssueSubscription(workspaceSlug, projectId, issueId);
const handleDeleteIssue = () => issueDetailStore.deleteIssue(workspaceSlug, projectId, issueId);
return (
<IssueView
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
states={states}
members={members}
priorities={priorities}
issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
@ -71,6 +68,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
handleDeleteIssue={handleDeleteIssue}
>
{children}
</IssueView>

View File

@ -1,17 +1,22 @@
import { FC, ReactNode, useEffect, useState } from "react";
import { FC, ReactNode, useState } from "react";
import { useRouter } from "next/router";
import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react";
import { PanelRightOpen, Square, SquareCode, MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
// components
import { PeekOverviewIssueDetails } from "./issue-detail";
import { PeekOverviewProperties } from "./properties";
import { IssueComment } from "./activity";
import { Button, CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui";
import { DeleteIssueModal } from "../delete-issue-modal";
// types
import { IIssue } from "types";
import { RootStore } from "store/root";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
interface IIssueView {
workspaceSlug: string;
@ -25,9 +30,9 @@ interface IIssueView {
issueCommentRemove: (commentId: string) => void;
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
states: any;
members: any;
priorities: any;
issueSubscriptionCreate: () => void;
issueSubscriptionRemove: () => void;
handleDeleteIssue: () => Promise<void>;
children: ReactNode;
}
@ -36,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen";
const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
{
key: "side-peek",
icon: PanelRightOpen,
icon: SidePeekIcon,
title: "Side Peek",
},
{
key: "modal",
icon: Square,
icon: ModalPeekIcon,
title: "Modal",
},
{
key: "full-screen",
icon: SquareCode,
icon: FullScreenPeekIcon,
title: "Full Screen",
},
];
@ -64,9 +69,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueCommentRemove,
issueCommentReactionCreate,
issueCommentReactionRemove,
states,
members,
priorities,
issueSubscriptionCreate,
issueSubscriptionRemove,
handleDeleteIssue,
children,
} = props;
@ -76,8 +81,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const handlePeekMode = (_peek: TPeekModes) => {
if (peekMode != _peek) setPeekMode(_peek);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
const { setToastAlert } = useToast();
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues/${peekIssueId}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Issue link copied to clipboard.",
});
});
};
const updateRoutePeekId = () => {
@ -117,128 +134,129 @@ export const IssueView: FC<IIssueView> = observer((props) => {
}
);
useSWR(
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
? `ISSUE_PEEK_OVERVIEW_SUBSCRIPTION_${workspaceSlug}_${projectId}_${peekIssueId}`
: null,
async () => {
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
await issueDetailStore.fetchIssueSubscription(workspaceSlug, projectId, issueId);
}
}
);
const issue = issueDetailStore.getIssue;
const issueReactions = issueDetailStore.getIssueReactions;
const issueComments = issueDetailStore.getIssueComments;
const issueSubscription = issueDetailStore.getIssueSubscription;
const user = userStore?.currentUser;
return (
<div className="w-full !text-base">
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
{children}
</div>
const currentMode = peekOptions.find((m) => m.key === peekMode);
{issueId === peekIssueId && (
<div
className={`fixed z-50 overflow-hidden bg-custom-background-80 flex flex-col transition-all duration-300 border border-custom-border-200 rounded shadow-custom-shadow-2xl
return (
<>
{issue && (
<DeleteIssueModal
isOpen={deleteIssueModal}
handleClose={() => setDeleteIssueModal(false)}
data={issue}
onSubmit={handleDeleteIssue}
/>
)}
<div className="w-full !text-base">
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
{children}
</div>
{issueId === peekIssueId && (
<div
className={`fixed z-20 overflow-hidden bg-custom-background-100 flex flex-col transition-all duration-300 border border-custom-border-200 rounded
${peekMode === "side-peek" ? `w-full md:w-[50%] top-0 right-0 bottom-0` : ``}
${peekMode === "modal" ? `top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] w-5/6 h-5/6` : ``}
${peekMode === "full-screen" ? `top-0 right-0 bottom-0 left-0 m-4` : ``}
`}
>
{/* header */}
<div className="flex-shrink-0 w-full p-4 py-3 relative flex items-center gap-2 border-b border-custom-border-200">
<div
className="flex-shrink-0 overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100"
onClick={removeRoutePeekId}
>
<ArrowRight width={12} strokeWidth={2} />
</div>
style={{
boxShadow:
"0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)",
}}
>
{/* header */}
<div className="relative flex items-center justify-between p-5 border-b border-custom-border-200">
<div className="flex items-center gap-4">
<button onClick={removeRoutePeekId}>
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
</button>
<div
className="flex-shrink-0 overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100"
onClick={redirectToIssueDetail}
>
<Maximize2 width={12} strokeWidth={2} />
</div>
<button onClick={redirectToIssueDetail}>
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
</button>
{currentMode && (
<div className="flex-shrink-0 flex items-center gap-2">
<CustomSelect
value={currentMode}
onChange={(val: any) => setPeekMode(val)}
customButton={
<button type="button" className="">
<currentMode.icon className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
</button>
}
>
{peekOptions.map((mode) => (
<CustomSelect.Option key={mode.key} value={mode.key}>
<div className="flex items-center gap-1.5">
<mode.icon className={`h-4 w-4 flex-shrink-0 -my-1 `} />
{mode.title}
</div>
</CustomSelect.Option>
))}
</CustomSelect>
</div>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{peekOptions.map((_option) => (
<div
key={_option?.key}
className={`px-1.5 min-w-6 h-6 flex justify-center items-center gap-1 rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100
${peekMode === _option?.key ? `bg-custom-background-100` : ``}
`}
onClick={() => handlePeekMode(_option?.key)}
<div className="flex items-center gap-4">
<Button
size="sm"
prependIcon={<Bell className="h-3 w-3" />}
variant="outline-primary"
onClick={() =>
issueSubscription && issueSubscription.subscribed
? issueSubscriptionRemove
: issueSubscriptionCreate
}
>
<_option.icon width={14} strokeWidth={2} />
<div className="text-xs font-medium">{_option?.title}</div>
</div>
))}
</div>
<div className="w-full flex justify-end items-center gap-2">
<div className="px-1.5 min-w-6 h-6 text-xs font-medium flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
Subscribe
</div>
<div className="overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
<Link width={12} strokeWidth={2} />
</div>
<div className="overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
<Trash width={12} strokeWidth={2} />
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
</Button>
<button onClick={handleCopyText}>
<Link2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200 -rotate-45" />
</button>
<button onClick={() => setDeleteIssueModal(true)}>
<Trash2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
</button>
</div>
</div>
</div>
{/* content */}
<div className="w-full h-full overflow-hidden overflow-y-auto">
{issueDetailStore?.loader && !issue ? (
<div className="text-center py-10">Loading...</div>
) : (
issue && (
<>
{["side-peek", "modal"].includes(peekMode) ? (
<div className="space-y-6 p-4 py-5">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
issueUpdate={issueUpdate}
issueReactions={issueReactions}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
<PeekOverviewProperties
issue={issue}
issueUpdate={issueUpdate}
states={states}
members={members}
priorities={priorities}
/>
<div className="border-t border-custom-border-400" />
<IssueComment
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
user={user}
issueComments={issueComments}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
) : (
<div className="w-full h-full flex">
<div className="w-full h-full space-y-6 p-4 py-5">
{/* content */}
<div className="w-full h-full overflow-hidden overflow-y-auto">
{issueDetailStore?.loader && !issue ? (
<div className="text-center py-10">Loading...</div>
) : (
issue && (
<>
{["side-peek", "modal"].includes(peekMode) ? (
<div className="flex flex-col gap-3 px-10 py-6">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
issueReactions={issueReactions}
issueUpdate={issueUpdate}
issueReactions={issueReactions}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
<div className="border-t border-custom-border-400" />
<PeekOverviewProperties issue={issue} issueUpdate={issueUpdate} user={user} />
<IssueComment
workspaceSlug={workspaceSlug}
@ -253,23 +271,46 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
<PeekOverviewProperties
issue={issue}
issueUpdate={issueUpdate}
states={states}
members={members}
priorities={priorities}
/>
) : (
<div className="w-full h-full flex">
<div className="w-full h-full space-y-6 p-4 py-5">
<PeekOverviewIssueDetails
workspaceSlug={workspaceSlug}
issue={issue}
issueReactions={issueReactions}
issueUpdate={issueUpdate}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
<div className="border-t border-custom-border-400" />
<IssueComment
workspaceSlug={workspaceSlug}
projectId={projectId}
issueId={issueId}
user={user}
issueComments={issueComments}
issueCommentCreate={issueCommentCreate}
issueCommentUpdate={issueCommentUpdate}
issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove}
/>
</div>
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
<PeekOverviewProperties issue={issue} issueUpdate={issueUpdate} user={user} />
</div>
</div>
</div>
)}
</>
)
)}
)}
</>
)
)}
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
);
});

View File

@ -16,13 +16,20 @@ type Props = {
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
const { estimatePoints } = useEstimateOption();
const currentEstimate = estimatePoints?.find((e) => e.key === value)?.value;
return (
<CustomSelect
value={value}
customButton={
<div className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5">
<Triangle className={`h-4 w-4 ${value !== null ? "text-custom-text-100" : "text-custom-text-200"}`} />
{estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
<div className="flex items-center gap-1.5 text-xs bg-custom-background-80 rounded px-2.5 py-0.5">
{currentEstimate ? (
<>
<Triangle className={`h-3 w-3 ${value !== null ? "text-custom-text-100" : "text-custom-text-200"}`} />
{currentEstimate}
</>
) : (
"No Estimate"
)}
</div>
}
onChange={onChange}
@ -31,7 +38,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
<CustomSelect.Option value={null}>
<>
<span>
<Triangle className="h-4 w-4" />
<Triangle className="h-3.5 w-3" />
</span>
None
</>
@ -41,7 +48,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
<CustomSelect.Option key={point.key} value={point.key}>
<>
<span>
<Triangle className="h-4 w-4" />
<Triangle className="h-3.5 w-3.5" />
</span>
{point.value}
</>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { Controller, UseFormWatch, useForm } from "react-hook-form";
import { Controller, useForm } from "react-hook-form";
import { TwitterPicker } from "react-color";
// headless ui
import { Listbox, Popover, Transition } from "@headlessui/react";
@ -12,7 +12,7 @@ import useUser from "hooks/use-user";
// ui
import { Input, Spinner } from "@plane/ui";
// icons
import { Component, Plus, Tag, X } from "lucide-react";
import { Component, Plus, X } from "lucide-react";
// types
import { IIssue, IIssueLabels } from "types";
// fetch-keys
@ -20,8 +20,7 @@ import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
type Props = {
issueDetails: IIssue | undefined;
issueControl: any;
watchIssue: UseFormWatch<IIssue>;
labelList: string[];
submitChanges: (formData: any) => void;
isNotAllowed: boolean;
uneditable: boolean;
@ -36,8 +35,7 @@ const issueLabelService = new IssueLabelService();
export const SidebarLabelSelect: React.FC<Props> = ({
issueDetails,
issueControl,
watchIssue,
labelList,
submitChanges,
isNotAllowed,
uneditable,
@ -91,167 +89,152 @@ export const SidebarLabelSelect: React.FC<Props> = ({
}, [createLabelForm, reset, setFocus]);
return (
<div className={`space-y-3 py-3 ${uneditable ? "opacity-60" : ""}`}>
<div className="flex items-start justify-between">
<div className="flex basis-1/2 items-center gap-x-2 text-sm text-custom-text-200">
<Tag className="h-4 w-4" />
<p>Label</p>
</div>
<div className="basis-1/2">
<div className="flex flex-wrap gap-1">
{watchIssue("labels")?.map((labelId) => {
const label = issueLabels?.find((l) => l.id === labelId);
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
<div className="flex flex-wrap gap-1">
{labelList?.map((labelId) => {
const label = issueLabels?.find((l) => l.id === labelId);
if (label)
return (
<span
key={label.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs hover:border-red-500/20 hover:bg-red-500/20"
onClick={() => {
const updatedLabels = watchIssue("labels")?.filter((l) => l !== labelId);
submitChanges({
labels: updatedLabels,
});
}}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
<X className="h-2 w-2 group-hover:text-red-500" />
</span>
);
})}
<Controller
control={issueControl}
name="labels"
render={({ field: { value } }) => (
<Listbox
as="div"
value={value}
onChange={(val: any) => submitChanges({ labels: val })}
className="flex-shrink-0"
multiple
disabled={isNotAllowed || uneditable}
>
{({ open }) => (
<div className="relative">
<Listbox.Button
className={`flex ${
isNotAllowed || uneditable
? "cursor-not-allowed"
: "cursor-pointer hover:bg-custom-background-90"
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
>
Select Label
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
<div className="py-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Listbox.Option
key={label.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-90" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={label.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</Listbox.Option>
);
} else
return (
<div className="border-y border-custom-border-100 bg-custom-background-90">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-custom-text-100">
<Component className="h-3 w-3" />
{label.name}
</div>
<div>
{children.map((child) => (
<Listbox.Option
key={child.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-100" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={child.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color ?? "black",
}}
/>
{child.name}
</Listbox.Option>
))}
</div>
</div>
);
})
) : (
<div className="text-center">No labels found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
)}
/>
{!isNotAllowed && (
<button
type="button"
if (label)
return (
<span
key={label.id}
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs hover:border-red-500/20 hover:bg-red-500/20"
onClick={() => {
const updatedLabels = labelList?.filter((l) => l !== labelId);
submitChanges({
labels: updatedLabels,
});
}}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
<X className="h-2 w-2 group-hover:text-red-500" />
</span>
);
})}
<Listbox
as="div"
value={issueDetails?.labels ?? []}
onChange={(val: any) => submitChanges({ labels: val })}
className="flex-shrink-0"
multiple
disabled={isNotAllowed || uneditable}
>
{({ open }) => (
<div className="relative">
<Listbox.Button
className={`flex ${
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
onClick={() => setCreateLabelForm((prevData) => !prevData)}
disabled={uneditable}
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
>
{createLabelForm ? (
<>
<X className="h-3 w-3" /> Cancel
</>
) : (
<>
<Plus className="h-3 w-3" /> New
</>
)}
</button>
Select Label
</Listbox.Button>
<Transition
show={open}
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
<div className="py-1">
{issueLabels ? (
issueLabels.length > 0 ? (
issueLabels.map((label: IIssueLabels) => {
const children = issueLabels?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Listbox.Option
key={label.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-90" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={label.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
}}
/>
{label.name}
</Listbox.Option>
);
} else
return (
<div className="border-y border-custom-border-100 bg-custom-background-90">
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-custom-text-100">
<Component className="h-3 w-3" />
{label.name}
</div>
<div>
{children.map((child) => (
<Listbox.Option
key={child.id}
className={({ active, selected }) =>
`${active || selected ? "bg-custom-background-100" : ""} ${
selected ? "" : "text-custom-text-200"
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
}
value={child.id}
>
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color ?? "black",
}}
/>
{child.name}
</Listbox.Option>
))}
</div>
</div>
);
})
) : (
<div className="text-center">No labels found</div>
)
) : (
<Spinner />
)}
</div>
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
{!isNotAllowed && (
<button
type="button"
className={`flex ${
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
onClick={() => setCreateLabelForm((prevData) => !prevData)}
disabled={uneditable}
>
{createLabelForm ? (
<>
<X className="h-3 w-3" /> Cancel
</>
) : (
<>
<Plus className="h-3 w-3" /> New
</>
)}
</div>
</div>
</button>
)}
</div>
{createLabelForm && (
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
<div>

View File

@ -32,7 +32,7 @@ import {
// ui
import { CustomDatePicker } from "components/ui";
// icons
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Trash2, Triangle, User2 } from "lucide-react";
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
@ -333,7 +333,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<p>State</p>
</div>
<div className="sm:basis-1/2">
<div>
<Controller
control={control}
name="state"
@ -354,7 +354,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p>
</div>
<div className="sm:basis-1/2">
<div>
<Controller
control={control}
name="assignees"
@ -375,7 +375,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Signal className="h-4 w-4 flex-shrink-0" />
<p>Priority</p>
</div>
<div className="sm:basis-1/2">
<div>
<Controller
control={control}
name="priority"
@ -583,7 +583,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
<p>Cycle</p>
</div>
<div className="space-y-1 sm:w-1/2">
<div className="space-y-1">
<SidebarCycleSelect
issueDetail={issueDetail}
handleCycleChange={handleCycleChange}
@ -598,7 +598,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<DiceIcon className="h-4 w-4 flex-shrink-0" />
<p>Module</p>
</div>
<div className="space-y-1 sm:w-1/2">
<div className="space-y-1">
<SidebarModuleSelect
issueDetail={issueDetail}
handleModuleChange={handleModuleChange}
@ -611,14 +611,21 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
)}
</div>
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<SidebarLabelSelect
issueDetails={issueDetail}
issueControl={control}
watchIssue={watchIssue}
submitChanges={submitChanges}
isNotAllowed={isNotAllowed}
uneditable={uneditable ?? false}
/>
<div className="flex flex-wrap items-start py-2">
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
<Tag className="h-4 w-4 flex-shrink-0" />
<p>Label</p>
</div>
<div className="space-y-1 sm:w-1/2">
<SidebarLabelSelect
issueDetails={issueDetail}
labelList={issueDetail?.labels ?? []}
submitChanges={submitChanges}
isNotAllowed={isNotAllowed}
uneditable={uneditable ?? false}
/>
</div>
</div>
)}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<div className={`min-h-[116px] py-1 text-xs ${uneditable ? "opacity-60" : ""}`}>

View File

@ -8,14 +8,12 @@ import { IssueService } from "services/issue";
import { TrackEventService } from "services/track_event.service";
// components
import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues";
import { MembersSelect, PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// helpers
import { getStatesList } from "helpers/state.helper";
import { PrioritySelect } from "components/project";
// types
import { IUser, IIssue, IState } from "types";
// fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys";
import { IssuePropertyAssignee, IssuePropertyState } from "../issue-layouts/properties";
export interface IIssueProperty {
workspaceSlug: string;
@ -32,7 +30,7 @@ const trackEventService = new TrackEventService();
export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
const { workspaceSlug, parentIssue, issue, user, editable } = props;
const { project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
const { issueFilter: issueFilterStore } = useMobxStore();
const displayProperties = issueFilterStore.userDisplayProperties ?? {};
@ -117,8 +115,6 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
);
};
const statesList = getStatesList(projectStore.states?.[issue.project]);
return (
<div className="relative flex items-center gap-1">
{displayProperties.priority && (
@ -134,12 +130,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
{displayProperties.state && (
<div className="flex-shrink-0">
<StateSelect
value={issue.state_detail}
states={statesList}
<IssuePropertyState
projectId={issue?.project_detail?.id || null}
value={issue?.state_detail || null}
onChange={(data) => handleStateChange(data)}
hideDropdownArrow
disabled={!editable}
disabled={false}
hideDropdownArrow={true}
/>
</div>
)}
@ -168,13 +164,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
{displayProperties.assignee && (
<div className="flex-shrink-0">
<MembersSelect
value={issue.assignees}
<IssuePropertyAssignee
projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
hideDropdownArrow={true}
onChange={(val) => handleAssigneeChange(val)}
members={projectStore.members ? (projectStore.members[issue.project] ?? []).map((m) => m.member) : []}
hideDropdownArrow
disabled={!editable}
multiple
disabled={false}
/>
</div>
)}

View File

@ -1,6 +1,7 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
// services
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
import { NotificationService } from "services/notification.service";
// types
import { RootStore } from "../root";
import { IIssue } from "types";
@ -28,6 +29,9 @@ export interface IIssueDetailStore {
[comment_id: string]: any;
};
};
issue_subscription: {
[issueId: string]: any;
};
setPeekId: (issueId: string | null) => void;
@ -36,6 +40,7 @@ export interface IIssueDetailStore {
getIssueReactions: any | null;
getIssueComments: any | null;
getIssueCommentReactions: any | null;
getIssueSubscription: any | null;
// fetch issue details
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
@ -84,6 +89,10 @@ export interface IIssueDetailStore {
commentId: string,
reaction: string
) => Promise<void>;
fetchIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
createIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
removeIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
}
export class IssueDetailStore implements IIssueDetailStore {
@ -103,6 +112,9 @@ export class IssueDetailStore implements IIssueDetailStore {
issue_comment_reactions: {
[issueId: string]: any;
} = {};
issue_subscription: {
[issueId: string]: any;
} = {};
// root store
rootStore;
@ -110,6 +122,7 @@ export class IssueDetailStore implements IIssueDetailStore {
issueService;
issueReactionService;
issueCommentService;
notificationService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@ -122,11 +135,13 @@ export class IssueDetailStore implements IIssueDetailStore {
issue_reactions: observable.ref,
issue_comments: observable.ref,
issue_comment_reactions: observable.ref,
issue_subscription: observable.ref,
getIssue: computed,
getIssueReactions: computed,
getIssueComments: computed,
getIssueCommentReactions: computed,
getIssueSubscription: computed,
setPeekId: action,
@ -150,12 +165,17 @@ export class IssueDetailStore implements IIssueDetailStore {
fetchIssueCommentReactions: action,
creationIssueCommentReaction: action,
removeIssueCommentReaction: action,
fetchIssueSubscription: action,
createIssueSubscription: action,
removeIssueSubscription: action,
});
this.rootStore = _rootStore;
this.issueService = new IssueService();
this.issueReactionService = new IssueReactionService();
this.issueCommentService = new IssueCommentService();
this.notificationService = new NotificationService();
}
get getIssue() {
@ -182,6 +202,12 @@ export class IssueDetailStore implements IIssueDetailStore {
return _commentReactions || null;
}
get getIssueSubscription() {
if (!this.peekId) return null;
const _commentSubscription = this.issue_subscription[this.peekId];
return _commentSubscription || null;
}
setPeekId = (issueId: string | null) => (this.peekId = issueId);
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
@ -662,4 +688,61 @@ export class IssueDetailStore implements IIssueDetailStore {
throw error;
}
};
// subscription
fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
workspaceSlug,
projectId,
issueId
);
const _issue_subscription = {
...this.issue_subscription,
[issueId]: _subscription,
};
runInAction(() => {
this.issue_subscription = _issue_subscription;
});
} catch (error) {
console.warn("error fetching the issue subscription", error);
throw error;
}
};
createIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
const _issue_subscription = {
...this.issue_subscription,
[issueId]: { subscribed: true },
};
runInAction(() => {
this.issue_subscription = _issue_subscription;
});
} catch (error) {
console.warn("error creating the issue subscription", error);
throw error;
}
};
removeIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
const _issue_subscription = {
...this.issue_subscription,
[issueId]: { subscribed: false },
};
runInAction(() => {
this.issue_subscription = _issue_subscription;
});
await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
console.warn("error removing the issue subscription", error);
throw error;
}
};
}