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 && ( {projectDetails?.is_deployed && deployUrl && (
<a <a
href={`${deployUrl}/${workspaceSlug}/${projectDetails?.id}`} 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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

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

View File

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

View File

@ -1,6 +1,6 @@
// components // components
import { KanbanIssueBlock } from "components/issues"; import { KanbanIssueBlock } from "components/issues";
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types"; import { IIssueDisplayProperties, IIssue } from "types";
interface IssueBlocksListProps { interface IssueBlocksListProps {
sub_group_id: string; sub_group_id: string;
@ -15,26 +15,10 @@ interface IssueBlocksListProps {
) => void; ) => void;
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
} }
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => { export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
const { const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
sub_group_id,
columnId,
issues,
isDragDisabled,
handleIssues,
quickActions,
displayProperties,
states,
labels,
members,
estimates,
} = props;
return ( return (
<> <>
@ -51,10 +35,6 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
columnId={columnId} columnId={columnId}
sub_group_id={sub_group_id} sub_group_id={sub_group_id}
isDragDisabled={isDragDisabled} 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 { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues"; import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
// types // types
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types"; import { IIssueDisplayProperties, IIssue } from "types";
// constants // constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
@ -30,11 +30,6 @@ export interface IGroupByKanBan {
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
priorities: any;
estimates: IEstimatePoint[] | null;
} }
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => { const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -51,11 +46,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
displayProperties, displayProperties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
states,
labels,
members,
priorities,
estimates,
enableQuickIssueCreate, enableQuickIssueCreate,
} = props; } = props;
@ -105,10 +95,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
displayProperties={displayProperties} displayProperties={displayProperties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/> />
) : ( ) : (
isDragDisabled && ( isDragDisabled && (
@ -154,13 +140,6 @@ export interface IKanBan {
displayProperties: IIssueDisplayProperties; displayProperties: IIssueDisplayProperties;
kanBanToggle: any; kanBanToggle: any;
handleKanBanToggle: any; handleKanBanToggle: any;
states: IState[] | null;
stateGroups: any;
priorities: any;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
estimates: IEstimatePoint[] | null;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
} }
@ -175,13 +154,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
displayProperties, displayProperties,
kanBanToggle, kanBanToggle,
handleKanBanToggle, handleKanBanToggle,
states,
stateGroups,
priorities,
labels,
members,
projects,
estimates,
enableQuickIssueCreate, enableQuickIssueCreate,
} = props; } = props;
@ -204,11 +176,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
kanBanToggle={kanBanToggle} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} 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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} 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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} 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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} 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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} 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} kanBanToggle={kanBanToggle}
handleKanBanToggle={handleKanBanToggle} handleKanBanToggle={handleKanBanToggle}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
states={states}
labels={labels}
members={members}
priorities={priorities}
estimates={estimates}
/> />
)} )}
</div> </div>

View File

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

View File

@ -116,13 +116,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties} displayProperties={displayProperties}
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} 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 <KanBanSwimLanes

View File

@ -116,13 +116,6 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties} displayProperties={displayProperties}
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} 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 <KanBanSwimLanes

View File

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

View File

@ -106,14 +106,6 @@ export const KanBanLayout: React.FC = observer(() => {
displayProperties={displayProperties} displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle} kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle} 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 <KanBanSwimLanes

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { FC } from "react";
// components // components
import { IssueBlock } from "components/issues"; import { IssueBlock } from "components/issues";
// types // types
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; import { IIssue } from "types";
interface Props { interface Props {
columnId: string; columnId: string;
@ -10,15 +10,10 @@ interface Props {
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
estimates: IEstimatePoint[] | null;
} }
export const IssueBlocksList: FC<Props> = (props) => { export const IssueBlocksList: FC<Props> = (props) => {
const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, estimates } = const { columnId, issues, handleIssues, quickActions, display_properties } = props;
props;
return ( return (
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200"> <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} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} 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; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any; display_properties: any;
is_list?: boolean; is_list?: boolean;
states: IState[] | null;
labels: IIssueLabels[] | null;
members: IUserLite[] | null;
projects: IProject[] | null;
stateGroups: any;
priorities: any;
enableQuickIssueCreate?: boolean; enableQuickIssueCreate?: boolean;
estimates: IEstimatePoint[] | null;
} }
const GroupByList: React.FC<IGroupByList> = observer((props) => { const GroupByList: React.FC<IGroupByList> = observer((props) => {
@ -37,13 +30,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
quickActions, quickActions,
display_properties, display_properties,
is_list = false, is_list = false,
states,
labels,
members,
projects,
stateGroups,
priorities,
estimates,
enableQuickIssueCreate, enableQuickIssueCreate,
} = props; } = props;
@ -70,10 +56,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
estimates={estimates}
/> />
)} )}
{enableQuickIssueCreate && ( {enableQuickIssueCreate && (
@ -121,7 +103,7 @@ export const List: React.FC<IList> = observer((props) => {
projects, projects,
stateGroups, stateGroups,
priorities, priorities,
estimates,
enableQuickIssueCreate, enableQuickIssueCreate,
} = props; } = props;
@ -137,13 +119,6 @@ export const List: React.FC<IList> = observer((props) => {
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
is_list is_list
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -157,13 +132,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -177,13 +145,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -197,13 +158,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -217,13 +171,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -237,13 +184,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -257,13 +197,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}
@ -277,13 +210,6 @@ export const List: React.FC<IList> = observer((props) => {
handleIssues={handleIssues} handleIssues={handleIssues}
quickActions={quickActions} quickActions={quickActions}
display_properties={display_properties} display_properties={display_properties}
states={states}
labels={labels}
members={members}
projects={projects}
stateGroups={stateGroups}
priorities={priorities}
estimates={estimates}
enableQuickIssueCreate={enableQuickIssueCreate} enableQuickIssueCreate={enableQuickIssueCreate}
/> />
)} )}

View File

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

View File

@ -1,28 +1,180 @@
import { Fragment, useState } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components import { useMobxStore } from "lib/mobx/store-provider";
import { MembersSelect } from "components/project";
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 // types
import { IUserLite } from "types"; import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyAssignee { export interface IIssuePropertyAssignee {
value: string[]; view?: "profile" | "workspace" | "project";
projectId: string | null;
value: string[] | string;
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
members: IUserLite[] | null;
disabled?: boolean; disabled?: boolean;
hideDropdownArrow?: boolean; hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
multiple?: true;
} }
export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer((props) => { 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 ( return (
<MembersSelect <Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
value={value} <Combobox.Button as={Fragment}>
onChange={onChange} <button
members={members ?? undefined} ref={setReferenceElement}
disabled={disabled} type="button"
hideDropdownArrow={hideDropdownArrow} className={`flex items-center justify-between gap-1 w-full text-xs ${
multiple 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 <Popover.Button
ref={dropdownBtn} 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" 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"; 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 // types
import { IEstimatePoint } from "types"; import { Placement } from "@popperjs/core";
export interface IIssuePropertyEstimates { export interface IIssuePropertyEstimates {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: number | null; value: number | null;
onChange: (value: number | null) => void; onChange: (value: number | null) => void;
estimatePoints: IEstimatePoint[] | null;
disabled?: boolean; disabled?: boolean;
hideDropdownArrow?: boolean; hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
} }
export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observer((props) => { 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 ( return (
<EstimateSelect <Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value} value={value}
onChange={onChange} onChange={(val) => onChange(val as number | null)}
estimatePoints={estimatePoints ?? undefined}
buttonClassName="h-5"
disabled={disabled} 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 { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import { usePopper } from "react-popper";
// components // components
import { LabelSelect } from "components/labels"; import { Combobox } from "@headlessui/react";
import { Tooltip } from "@plane/ui";
import { Check, ChevronDown, Search } from "lucide-react";
// types // types
import { IIssueLabels } from "types"; import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyLabels { export interface IIssuePropertyLabels {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: string[]; value: string[];
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
labels: IIssueLabels[] | null;
disabled?: boolean; disabled?: boolean;
hideDropdownArrow?: boolean; hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
maxRender?: number;
} }
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => { 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 ( return (
<LabelSelect <Combobox
as="div"
className={`flex-shrink-0 text-left ${className}`}
value={value} value={value}
onChange={onChange} onChange={onChange}
labels={labels ?? undefined}
buttonClassName="h-5"
disabled={disabled} 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"; import { observer } from "mobx-react-lite";
// components import { useMobxStore } from "lib/mobx/store-provider";
import { StateSelect } from "components/states";
// 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 // types
import { IState } from "types"; import { IState } from "types";
import { Placement } from "@popperjs/core";
import { RootStore } from "store/root";
export interface IIssuePropertyState { export interface IIssuePropertyState {
view?: "profile" | "workspace" | "project";
projectId: string | null;
value: IState; value: IState;
onChange: (state: IState) => void; onChange: (state: IState) => void;
states: IState[] | null;
disabled?: boolean; disabled?: boolean;
hideDropdownArrow?: boolean; hideDropdownArrow?: boolean;
className?: string;
buttonClassName?: string;
optionsClassName?: string;
placement?: Placement;
} }
export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props) => { 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 ( return (
<StateSelect <>
value={value} {workspaceSlug && projectId && (
onChange={onChange} <Combobox
states={states ?? undefined} as="div"
buttonClassName="h-5" className={`flex-shrink-0 text-left ${className}`}
disabled={disabled} value={value.id}
hideDropdownArrow={hideDropdownArrow} 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"; import React from "react";
// components // components
import { MembersSelect } from "components/project"; import { IssuePropertyAssignee } from "../../properties";
// hooks // hooks
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // 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); const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
return ( return (
<div className="flex items-center h-full px-4"> <>
<MembersSelect <IssuePropertyAssignee
projectId={issue.project_detail.id ?? null}
value={issue.assignees} value={issue.assignees}
onChange={(data) => onChange({ assignees: data })} onChange={(data) => onChange({ assignees: data })}
members={members ?? []}
buttonClassName="!p-0 !rounded-none !border-0" buttonClassName="!p-0 !rounded-none !border-0"
hideDropdownArrow hideDropdownArrow
disabled={disabled} disabled={disabled}
@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
disabled={disabled} disabled={disabled}
/> />
))} ))}
</div> </>
); );
}; };

View File

@ -1,5 +1,5 @@
// components // components
import { EstimateSelect } from "components/estimates"; import { IssuePropertyEstimates } from "../../properties";
// hooks // hooks
import useSubIssue from "hooks/use-sub-issue"; import useSubIssue from "hooks/use-sub-issue";
// types // types
@ -21,14 +21,12 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
return ( return (
<> <>
<EstimateSelect <IssuePropertyEstimates
projectId={issue.project_detail.id ?? null}
value={issue.estimate_point} value={issue.estimate_point}
onChange={(data) => onChange({ estimate_point: data })} 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 hideDropdownArrow
disabled={disabled}
/> />
{isExpanded && {isExpanded &&

View File

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

View File

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

View File

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

View File

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

View File

@ -36,8 +36,8 @@ export const IssueComment: FC<IIssueComment> = (props) => {
}; };
return ( return (
<div className="space-y-4"> <div className="flex flex-col gap-3 border-t py-6 border-custom-border-200">
<div className="font-medium text-xl">Activity</div> <div className="font-medium text-lg">Activity</div>
<div className="space-y-2"> <div className="space-y-2">
<IssueCommentEditor <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 // packages
import { RichTextEditor } from "@plane/rich-text-editor"; import { RichTextEditor } from "@plane/rich-text-editor";
// components // components
import { TextArea } from "@plane/ui";
import { IssueReaction } from "./reactions"; import { IssueReaction } from "./reactions";
// hooks // hooks
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import useReloadConfirmations from "hooks/use-reload-confirmation";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
// services // services
import { FileService } from "services/file.service"; import { FileService } from "services/file.service";
import { useForm, Controller } from "react-hook-form";
import useReloadConfirmations from "hooks/use-reload-confirmation";
const fileService = new FileService(); const fileService = new FileService();
@ -26,24 +27,45 @@ interface IPeekOverviewIssueDetails {
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => { export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props; const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); 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: { defaultValues: {
name: "", name: "",
description_html: "", 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(() => { await issueUpdate({
if (!issue) return; ...issue,
name: formData.name ?? "",
description_html: formData.description_html ?? "<p></p>",
});
},
[issue, issueUpdate]
);
reset({ const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
...issue, issueUpdate({ ...issue, description_html: _data });
}); }, 1500);
}, [issue, reset]);
const debouncedTitleSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500);
useEffect(() => { useEffect(() => {
if (isSubmitting === "submitted") { if (isSubmitting === "submitted") {
@ -56,62 +78,80 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
} }
}, [isSubmitting, setShowAlert]); }, [isSubmitting, setShowAlert]);
const handleDescriptionFormSubmit = useCallback( // reset form values
async (formData: Partial<IIssue>) => { useEffect(() => {
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; if (!issue) return;
issueUpdate({ name: formData.name ?? "", description_html: formData.description_html }); reset({
}, ...issue,
[issueUpdate] });
); }, [issue, reset]);
const debouncedIssueFormSave = useDebouncedCallback(async () => {
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
}, 1500);
return ( 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} {issue?.project_detail?.identifier}-{issue?.sequence_id}
</div> </span>
<div className="font-medium text-xl">{watch("name")}</div> <div className="relative">
{true ? (
<div className="space-y-2">
<div className="relative">
<Controller <Controller
name="description_html" name="name"
control={control} control={control}
render={({ field: { value, onChange } }) => ( render={({ field: { value, onChange } }) => (
<RichTextEditor <TextArea
uploadFile={fileService.getUploadFileFunction(workspaceSlug)} id="name"
deleteFile={fileService.deleteImage} name="name"
value={value} value={value}
onChange={(description: Object, description_html: string) => { placeholder="Enter issue name"
onFocus={() => setCharacterLimit(true)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
setCharacterLimit(false);
setIsSubmitting("submitting"); setIsSubmitting("submitting");
onChange(description_html); debouncedTitleSave();
debouncedIssueFormSave(); 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 ${ <h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
isSubmitting === "saved" ? "fadeOut" : "fadeIn" )}
}`} {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">
{isSubmitting === "submitting" ? "Saving..." : "Saved"} <span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
{watch("name").length}
</span>
/255
</div> </div>
</div> )}
<IssueReaction
issueReactions={issueReactions}
user={user}
issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove}
/>
</div> </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 // ui icons
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui"; import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
import { CalendarDays, Signal } from "lucide-react"; 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 // components
import { IssuePropertyState } from "components/issues/issue-layouts/properties/state"; import { CustomDatePicker } from "components/ui";
import { IssuePropertyPriority } from "components/issues/issue-layouts/properties/priority"; import { LinkModal, LinksList } from "components/core";
import { IssuePropertyAssignee } from "components/issues/issue-layouts/properties/assignee";
import { IssuePropertyDate } from "components/issues/issue-layouts/properties/date";
// types // 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 { interface IPeekOverviewProperties {
issue: IIssue; issue: IIssue;
issueUpdate: (issue: Partial<IIssue>) => void; issueUpdate: (issue: Partial<IIssue>) => void;
states: IState[] | null; user: any;
members: IUserLite[] | null;
priorities: any;
} }
const issueService = new IssueService();
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = (props) => { 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) => { const router = useRouter();
issueUpdate({ ...issue, state: _state.id }); const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const { memberRole } = useProjectMyMembership();
const handleState = (_state: string) => {
issueUpdate({ ...issue, state: _state });
}; };
const handlePriority = (_priority: TIssuePriorities) => { const handlePriority = (_priority: TIssuePriorities) => {
issueUpdate({ ...issue, priority: _priority }); issueUpdate({ ...issue, priority: _priority });
}; };
const handleAssignee = (_assignees: string[]) => { const handleAssignee = (_assignees: string[]) => {
issueUpdate({ ...issue, assignees: _assignees }); issueUpdate({ ...issue, assignees: _assignees });
}; };
const handleEstimate = (_estimate: number | null) => {
const handleStartDate = (_startDate: string) => { issueUpdate({ ...issue, estimate_point: _estimate });
};
const handleStartDate = (_startDate: string | null) => {
issueUpdate({ ...issue, start_date: _startDate }); issueUpdate({ ...issue, start_date: _startDate });
}; };
const handleTargetDate = (_targetDate: string | null) => {
const handleTargetDate = (_targetDate: string) => {
issueUpdate({ ...issue, target_date: _targetDate }); 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 ( return (
<div className="space-y-4"> <>
{/* state */} <LinkModal
<div className="flex items-center gap-2"> isOpen={linkModal}
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap"> handleClose={() => {
<div className="w-4 h-4 flex justify-center items-center overflow-hidden"> setLinkModal(false);
<DoubleCircleIcon className="h-3.5 w-3.5 flex-shrink-0" /> 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>
<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 */} {/* assignee */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap"> <div className="flex items-center gap-2 w-40">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden"> <UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<UserGroupIcon className="h-3.5 w-3.5" /> <p>Assignees</p>
</div>
<div>
<SidebarAssigneeSelect value={issue.assignees || []} onChange={handleAssignee} disabled={isNotAllowed} />
</div>
</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 */} {/* priority */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap"> <div className="flex items-center gap-2 w-40">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden"> <Signal className="h-4 w-4 flex-shrink-0" />
<Signal className="h-3.5 w-3.5" /> <p>Priority</p>
</div>
<div>
<SidebarPrioritySelect value={issue.priority || ""} onChange={handlePriority} disabled={isNotAllowed} />
</div>
</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 */} {/* estimate */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap"> <div className="flex items-center gap-2 w-40">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden"> <Triangle className="h-4 w-4 flex-shrink-0 " />
<CalendarDays className="h-3.5 w-3.5" /> <p>Estimate</p>
</div>
<div>
<SidebarEstimateSelect value={issue.estimate_point} onChange={handleEstimate} disabled={isNotAllowed} />
</div>
</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 */} {/* start date */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full">
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap"> <div className="flex items-center gap-2 w-40">
<div className="w-4 h-4 flex justify-center items-center overflow-hidden"> <CalendarDays className="h-4 w-4 flex-shrink-0" />
<CalendarDays className="h-3.5 w-3.5" /> <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>
<div className="font-medium text-custom-text-200 line-clamp-1">Target date</div>
</div> </div>
<div className="w-full">
<IssuePropertyDate <span className="border-t border-custom-border-200" />
value={issue?.target_date || null}
onChange={(date: string) => handleTargetDate(date)} <div className="flex flex-col gap-5 py-5 w-full">
disabled={false} <div className="flex items-center gap-2 w-80">
placeHolder={`Target date`} <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> </div>
</div> </>
); );
}; };

View File

@ -26,13 +26,13 @@ export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
type="button" type="button"
onClick={() => handleReaction(reaction)} onClick={() => handleReaction(reaction)}
key={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]) 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` : `bg-custom-background-90 hover:bg-custom-background-100/30`
}`} }`}
> >
<span>{renderEmoji(reaction)}</span> <span className="text-sm">{renderEmoji(reaction)}</span>
<span <span
className={`${ className={`${
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : `` 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 <Popover.Button
className={`${ className={`${
open ? "" : "bg-custom-background-90" open ? "" : "bg-custom-background-80"
} group inline-flex items-center rounded-md bg-custom-background-90 focus:outline-none transition-all hover:bg-custom-background-100`} } group inline-flex items-center rounded-md bg-custom-background-80 focus:outline-none transition-all hover:bg-custom-background-90`}
> >
<span <span className={`flex justify-center items-center rounded px-2 py-1.5`}>
className={`flex justify-center items-center rounded-md px-2 ${ <SmilePlus className={`${size === "sm" ? "w-3 h-3" : size === "md" ? "w-3.5 h-3.5" : "w-4 h-4"}`} />
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> </span>
</Popover.Button> </Popover.Button>
<Transition <Transition

View File

@ -7,8 +7,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
// constants
import { ISSUE_PRIORITIES } from "constants/issue";
interface IIssuePeekOverview { interface IIssuePeekOverview {
workspaceSlug: string; workspaceSlug: string;
@ -21,11 +19,7 @@ interface IIssuePeekOverview {
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => { export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { workspaceSlug, projectId, issueId, handleIssue, children } = props; const { workspaceSlug, projectId, issueId, handleIssue, children } = props;
const { project: projectStore, issueDetail: issueDetailStore }: RootStore = useMobxStore(); const { issueDetail: issueDetailStore }: RootStore = useMobxStore();
const states = projectStore?.projectStates || undefined;
const members = projectStore?.projectMembers || undefined;
const priorities = ISSUE_PRIORITIES || undefined;
const issueUpdate = (_data: Partial<IIssue>) => { const issueUpdate = (_data: Partial<IIssue>) => {
if (handleIssue) { if (handleIssue) {
@ -55,14 +49,17 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issueCommentReactionRemove = (commentId: string, reaction: string) => const issueCommentReactionRemove = (commentId: string, reaction: string) =>
issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction); 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 ( return (
<IssueView <IssueView
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
projectId={projectId} projectId={projectId}
issueId={issueId} issueId={issueId}
states={states}
members={members}
priorities={priorities}
issueUpdate={issueUpdate} issueUpdate={issueUpdate}
issueReactionCreate={issueReactionCreate} issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove} issueReactionRemove={issueReactionRemove}
@ -71,6 +68,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
issueCommentRemove={issueCommentRemove} issueCommentRemove={issueCommentRemove}
issueCommentReactionCreate={issueCommentReactionCreate} issueCommentReactionCreate={issueCommentReactionCreate}
issueCommentReactionRemove={issueCommentReactionRemove} issueCommentReactionRemove={issueCommentReactionRemove}
issueSubscriptionCreate={issueSubscriptionCreate}
issueSubscriptionRemove={issueSubscriptionRemove}
handleDeleteIssue={handleDeleteIssue}
> >
{children} {children}
</IssueView> </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 { 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 { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// components // components
import { PeekOverviewIssueDetails } from "./issue-detail"; import { PeekOverviewIssueDetails } from "./issue-detail";
import { PeekOverviewProperties } from "./properties"; import { PeekOverviewProperties } from "./properties";
import { IssueComment } from "./activity"; import { IssueComment } from "./activity";
import { Button, CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui";
import { DeleteIssueModal } from "../delete-issue-modal";
// types // types
import { IIssue } from "types"; import { IIssue } from "types";
import { RootStore } from "store/root"; import { RootStore } from "store/root";
// hooks // hooks
import { useMobxStore } from "lib/mobx/store-provider"; import { useMobxStore } from "lib/mobx/store-provider";
import useToast from "hooks/use-toast";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
interface IIssueView { interface IIssueView {
workspaceSlug: string; workspaceSlug: string;
@ -25,9 +30,9 @@ interface IIssueView {
issueCommentRemove: (commentId: string) => void; issueCommentRemove: (commentId: string) => void;
issueCommentReactionCreate: (commentId: string, reaction: string) => void; issueCommentReactionCreate: (commentId: string, reaction: string) => void;
issueCommentReactionRemove: (commentId: string, reaction: string) => void; issueCommentReactionRemove: (commentId: string, reaction: string) => void;
states: any; issueSubscriptionCreate: () => void;
members: any; issueSubscriptionRemove: () => void;
priorities: any; handleDeleteIssue: () => Promise<void>;
children: ReactNode; children: ReactNode;
} }
@ -36,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen";
const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [ const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
{ {
key: "side-peek", key: "side-peek",
icon: PanelRightOpen, icon: SidePeekIcon,
title: "Side Peek", title: "Side Peek",
}, },
{ {
key: "modal", key: "modal",
icon: Square, icon: ModalPeekIcon,
title: "Modal", title: "Modal",
}, },
{ {
key: "full-screen", key: "full-screen",
icon: SquareCode, icon: FullScreenPeekIcon,
title: "Full Screen", title: "Full Screen",
}, },
]; ];
@ -64,9 +69,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueCommentRemove, issueCommentRemove,
issueCommentReactionCreate, issueCommentReactionCreate,
issueCommentReactionRemove, issueCommentReactionRemove,
states, issueSubscriptionCreate,
members, issueSubscriptionRemove,
priorities, handleDeleteIssue,
children, children,
} = props; } = props;
@ -76,8 +81,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore(); const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek"); const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
const handlePeekMode = (_peek: TPeekModes) => { const [deleteIssueModal, setDeleteIssueModal] = useState(false);
if (peekMode != _peek) setPeekMode(_peek);
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 = () => { 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 issue = issueDetailStore.getIssue;
const issueReactions = issueDetailStore.getIssueReactions; const issueReactions = issueDetailStore.getIssueReactions;
const issueComments = issueDetailStore.getIssueComments; const issueComments = issueDetailStore.getIssueComments;
const issueSubscription = issueDetailStore.getIssueSubscription;
const user = userStore?.currentUser; const user = userStore?.currentUser;
return ( const currentMode = peekOptions.find((m) => m.key === peekMode);
<div className="w-full !text-base">
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
{children}
</div>
{issueId === peekIssueId && ( return (
<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 {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 === "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 === "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` : ``} ${peekMode === "full-screen" ? `top-0 right-0 bottom-0 left-0 m-4` : ``}
`} `}
> style={{
{/* header */} boxShadow:
<div className="flex-shrink-0 w-full p-4 py-3 relative flex items-center gap-2 border-b border-custom-border-200"> "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)",
<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} {/* header */}
> <div className="relative flex items-center justify-between p-5 border-b border-custom-border-200">
<ArrowRight width={12} strokeWidth={2} /> <div className="flex items-center gap-4">
</div> <button onClick={removeRoutePeekId}>
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
</button>
<div <button onClick={redirectToIssueDetail}>
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" <MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
onClick={redirectToIssueDetail} </button>
> {currentMode && (
<Maximize2 width={12} strokeWidth={2} /> <div className="flex-shrink-0 flex items-center gap-2">
</div> <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"> <div className="flex items-center gap-4">
{peekOptions.map((_option) => ( <Button
<div size="sm"
key={_option?.key} prependIcon={<Bell className="h-3 w-3" />}
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 variant="outline-primary"
${peekMode === _option?.key ? `bg-custom-background-100` : ``} onClick={() =>
`} issueSubscription && issueSubscription.subscribed
onClick={() => handlePeekMode(_option?.key)} ? issueSubscriptionRemove
: issueSubscriptionCreate
}
> >
<_option.icon width={14} strokeWidth={2} /> {issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
<div className="text-xs font-medium">{_option?.title}</div> </Button>
</div> <button onClick={handleCopyText}>
))} <Link2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200 -rotate-45" />
</div> </button>
<button onClick={() => setDeleteIssueModal(true)}>
<div className="w-full flex justify-end items-center gap-2"> <Trash2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
<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"> </button>
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} />
</div> </div>
</div> </div>
</div>
{/* content */} {/* content */}
<div className="w-full h-full overflow-hidden overflow-y-auto"> <div className="w-full h-full overflow-hidden overflow-y-auto">
{issueDetailStore?.loader && !issue ? ( {issueDetailStore?.loader && !issue ? (
<div className="text-center py-10">Loading...</div> <div className="text-center py-10">Loading...</div>
) : ( ) : (
issue && ( issue && (
<> <>
{["side-peek", "modal"].includes(peekMode) ? ( {["side-peek", "modal"].includes(peekMode) ? (
<div className="space-y-6 p-4 py-5"> <div className="flex flex-col gap-3 px-10 py-6">
<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">
<PeekOverviewIssueDetails <PeekOverviewIssueDetails
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
issue={issue} issue={issue}
issueReactions={issueReactions}
issueUpdate={issueUpdate} issueUpdate={issueUpdate}
issueReactions={issueReactions}
user={user} user={user}
issueReactionCreate={issueReactionCreate} issueReactionCreate={issueReactionCreate}
issueReactionRemove={issueReactionRemove} issueReactionRemove={issueReactionRemove}
/> />
<div className="border-t border-custom-border-400" /> <PeekOverviewProperties issue={issue} issueUpdate={issueUpdate} user={user} />
<IssueComment <IssueComment
workspaceSlug={workspaceSlug} workspaceSlug={workspaceSlug}
@ -253,23 +271,46 @@ export const IssueView: FC<IIssueView> = observer((props) => {
issueCommentReactionRemove={issueCommentReactionRemove} issueCommentReactionRemove={issueCommentReactionRemove}
/> />
</div> </div>
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5"> ) : (
<PeekOverviewProperties <div className="w-full h-full flex">
issue={issue} <div className="w-full h-full space-y-6 p-4 py-5">
issueUpdate={issueUpdate} <PeekOverviewIssueDetails
states={states} workspaceSlug={workspaceSlug}
members={members} issue={issue}
priorities={priorities} 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> )}
)} </div>
</div> </>
); );
}); });

View File

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

View File

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

View File

@ -32,7 +32,7 @@ import {
// ui // ui
import { CustomDatePicker } from "components/ui"; import { CustomDatePicker } from "components/ui";
// icons // 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"; import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
// helpers // helpers
import { copyTextToClipboard } from "helpers/string.helper"; 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" /> <DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
<p>State</p> <p>State</p>
</div> </div>
<div className="sm:basis-1/2"> <div>
<Controller <Controller
control={control} control={control}
name="state" name="state"
@ -354,7 +354,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<UserGroupIcon className="h-4 w-4 flex-shrink-0" /> <UserGroupIcon className="h-4 w-4 flex-shrink-0" />
<p>Assignees</p> <p>Assignees</p>
</div> </div>
<div className="sm:basis-1/2"> <div>
<Controller <Controller
control={control} control={control}
name="assignees" name="assignees"
@ -375,7 +375,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<Signal className="h-4 w-4 flex-shrink-0" /> <Signal className="h-4 w-4 flex-shrink-0" />
<p>Priority</p> <p>Priority</p>
</div> </div>
<div className="sm:basis-1/2"> <div>
<Controller <Controller
control={control} control={control}
name="priority" name="priority"
@ -583,7 +583,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<ContrastIcon className="h-4 w-4 flex-shrink-0" /> <ContrastIcon className="h-4 w-4 flex-shrink-0" />
<p>Cycle</p> <p>Cycle</p>
</div> </div>
<div className="space-y-1 sm:w-1/2"> <div className="space-y-1">
<SidebarCycleSelect <SidebarCycleSelect
issueDetail={issueDetail} issueDetail={issueDetail}
handleCycleChange={handleCycleChange} handleCycleChange={handleCycleChange}
@ -598,7 +598,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
<DiceIcon className="h-4 w-4 flex-shrink-0" /> <DiceIcon className="h-4 w-4 flex-shrink-0" />
<p>Module</p> <p>Module</p>
</div> </div>
<div className="space-y-1 sm:w-1/2"> <div className="space-y-1">
<SidebarModuleSelect <SidebarModuleSelect
issueDetail={issueDetail} issueDetail={issueDetail}
handleModuleChange={handleModuleChange} handleModuleChange={handleModuleChange}
@ -611,14 +611,21 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
)} )}
</div> </div>
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
<SidebarLabelSelect <div className="flex flex-wrap items-start py-2">
issueDetails={issueDetail} <div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
issueControl={control} <Tag className="h-4 w-4 flex-shrink-0" />
watchIssue={watchIssue} <p>Label</p>
submitChanges={submitChanges} </div>
isNotAllowed={isNotAllowed} <div className="space-y-1 sm:w-1/2">
uneditable={uneditable ?? false} <SidebarLabelSelect
/> issueDetails={issueDetail}
labelList={issueDetail?.labels ?? []}
submitChanges={submitChanges}
isNotAllowed={isNotAllowed}
uneditable={uneditable ?? false}
/>
</div>
</div>
)} )}
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
<div className={`min-h-[116px] py-1 text-xs ${uneditable ? "opacity-60" : ""}`}> <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"; import { TrackEventService } from "services/track_event.service";
// components // components
import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues"; import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues";
import { MembersSelect, PrioritySelect } from "components/project"; import { PrioritySelect } from "components/project";
import { StateSelect } from "components/states";
// helpers
import { getStatesList } from "helpers/state.helper";
// types // types
import { IUser, IIssue, IState } from "types"; import { IUser, IIssue, IState } from "types";
// fetch-keys // fetch-keys
import { SUB_ISSUES } from "constants/fetch-keys"; import { SUB_ISSUES } from "constants/fetch-keys";
import { IssuePropertyAssignee, IssuePropertyState } from "../issue-layouts/properties";
export interface IIssueProperty { export interface IIssueProperty {
workspaceSlug: string; workspaceSlug: string;
@ -32,7 +30,7 @@ const trackEventService = new TrackEventService();
export const IssueProperty: React.FC<IIssueProperty> = observer((props) => { export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
const { workspaceSlug, parentIssue, issue, user, editable } = props; const { workspaceSlug, parentIssue, issue, user, editable } = props;
const { project: projectStore, issueFilter: issueFilterStore } = useMobxStore(); const { issueFilter: issueFilterStore } = useMobxStore();
const displayProperties = issueFilterStore.userDisplayProperties ?? {}; const displayProperties = issueFilterStore.userDisplayProperties ?? {};
@ -117,8 +115,6 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
); );
}; };
const statesList = getStatesList(projectStore.states?.[issue.project]);
return ( return (
<div className="relative flex items-center gap-1"> <div className="relative flex items-center gap-1">
{displayProperties.priority && ( {displayProperties.priority && (
@ -134,12 +130,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
{displayProperties.state && ( {displayProperties.state && (
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<StateSelect <IssuePropertyState
value={issue.state_detail} projectId={issue?.project_detail?.id || null}
states={statesList} value={issue?.state_detail || null}
onChange={(data) => handleStateChange(data)} onChange={(data) => handleStateChange(data)}
hideDropdownArrow disabled={false}
disabled={!editable} hideDropdownArrow={true}
/> />
</div> </div>
)} )}
@ -168,13 +164,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
{displayProperties.assignee && ( {displayProperties.assignee && (
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<MembersSelect <IssuePropertyAssignee
value={issue.assignees} projectId={issue?.project_detail?.id || null}
value={issue?.assignees || null}
hideDropdownArrow={true}
onChange={(val) => handleAssigneeChange(val)} onChange={(val) => handleAssigneeChange(val)}
members={projectStore.members ? (projectStore.members[issue.project] ?? []).map((m) => m.member) : []} disabled={false}
hideDropdownArrow
disabled={!editable}
multiple
/> />
</div> </div>
)} )}

View File

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