mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
10e35d9a06
commit
1be82814fc
@ -130,7 +130,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
|
||||
{projectDetails?.is_deployed && deployUrl && (
|
||||
<a
|
||||
href={`${deployUrl}/${workspaceSlug}/${projectDetails?.id}`}
|
||||
className="group bg-custom-primary-100/20 text-custom-primary-100 px-2.5 py-1 text-xs flex items-center gap-1.5 rounded font-medium"
|
||||
className="group bg-custom-primary-100/10 text-custom-primary-100 px-2.5 py-1 text-xs flex items-center gap-1.5 rounded font-medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -10,4 +10,6 @@ export * from "./gantt";
|
||||
export * from "./kanban";
|
||||
export * from "./spreadsheet";
|
||||
|
||||
export * from "./properties";
|
||||
|
||||
export * from "./roots";
|
||||
|
@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd";
|
||||
// components
|
||||
import { KanBanProperties } from "./properties";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
sub_group_id: string;
|
||||
@ -18,27 +18,10 @@ interface IssueBlockProps {
|
||||
) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId,
|
||||
index,
|
||||
issue,
|
||||
isDragDisabled,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
|
||||
|
||||
const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => {
|
||||
if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update");
|
||||
@ -82,10 +65,6 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
issue={issue}
|
||||
handleIssues={updateIssue}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// components
|
||||
import { KanbanIssueBlock } from "components/issues";
|
||||
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IState, IUserLite } from "types";
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
|
||||
interface IssueBlocksListProps {
|
||||
sub_group_id: string;
|
||||
@ -15,26 +15,10 @@ interface IssueBlocksListProps {
|
||||
) => void;
|
||||
quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) => {
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId,
|
||||
issues,
|
||||
isDragDisabled,
|
||||
handleIssues,
|
||||
quickActions,
|
||||
displayProperties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, displayProperties } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -51,10 +35,6 @@ export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = (props) =>
|
||||
columnId={columnId}
|
||||
sub_group_id={sub_group_id}
|
||||
isDragDisabled={isDragDisabled}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -7,7 +7,7 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import { KanBanGroupByHeaderRoot } from "./headers/group-by-root";
|
||||
import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabels, IProject, IState, IUserLite } from "types";
|
||||
import { IIssueDisplayProperties, IIssue } from "types";
|
||||
// constants
|
||||
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue";
|
||||
|
||||
@ -30,11 +30,6 @@ export interface IGroupByKanBan {
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
priorities: any;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
@ -51,11 +46,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
displayProperties,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
priorities,
|
||||
estimates,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
@ -105,10 +95,6 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
displayProperties={displayProperties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
) : (
|
||||
isDragDisabled && (
|
||||
@ -154,13 +140,6 @@ export interface IKanBan {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
kanBanToggle: any;
|
||||
handleKanBanToggle: any;
|
||||
states: IState[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
}
|
||||
|
||||
@ -175,13 +154,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
displayProperties,
|
||||
kanBanToggle,
|
||||
handleKanBanToggle,
|
||||
states,
|
||||
stateGroups,
|
||||
priorities,
|
||||
labels,
|
||||
members,
|
||||
projects,
|
||||
estimates,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
@ -204,11 +176,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -227,11 +194,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -250,11 +212,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -273,11 +230,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -296,11 +248,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -319,11 +266,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -10,15 +10,7 @@ import { IssuePropertyAssignee } from "../properties/assignee";
|
||||
import { IssuePropertyEstimates } from "../properties/estimates";
|
||||
import { IssuePropertyDate } from "../properties/date";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import {
|
||||
IEstimatePoint,
|
||||
IIssue,
|
||||
IIssueDisplayProperties,
|
||||
IIssueLabels,
|
||||
IState,
|
||||
IUserLite,
|
||||
TIssuePriorities,
|
||||
} from "types";
|
||||
import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types";
|
||||
|
||||
export interface IKanBanProperties {
|
||||
sub_group_id: string;
|
||||
@ -26,24 +18,10 @@ export interface IKanBanProperties {
|
||||
issue: IIssue;
|
||||
handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void;
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) => {
|
||||
const {
|
||||
sub_group_id,
|
||||
columnId: group_id,
|
||||
issue,
|
||||
handleIssues,
|
||||
displayProperties,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
estimates,
|
||||
} = props;
|
||||
const { sub_group_id, columnId: group_id, issue, handleIssues, displayProperties } = props;
|
||||
|
||||
const handleState = (state: IState) => {
|
||||
handleIssues(
|
||||
@ -107,9 +85,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{/* state */}
|
||||
{displayProperties && displayProperties?.state && (
|
||||
<IssuePropertyState
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state_detail || null}
|
||||
onChange={handleState}
|
||||
states={states}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
@ -128,9 +106,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{/* label */}
|
||||
{displayProperties && displayProperties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.labels || null}
|
||||
onChange={handleLabel}
|
||||
labels={labels}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
@ -139,10 +117,10 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{/* assignee */}
|
||||
{displayProperties && displayProperties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleAssignee}
|
||||
members={members}
|
||||
disabled={false}
|
||||
/>
|
||||
)}
|
||||
@ -170,9 +148,9 @@ export const KanBanProperties: React.FC<IKanBanProperties> = observer((props) =>
|
||||
{/* estimates */}
|
||||
{displayProperties && displayProperties?.estimate && (
|
||||
<IssuePropertyEstimates
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.estimate_point || null}
|
||||
onChange={handleEstimate}
|
||||
estimatePoints={estimates}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
|
@ -116,13 +116,6 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
|
@ -116,13 +116,6 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
|
@ -99,13 +99,6 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => {
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
estimates={null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
|
@ -106,14 +106,6 @@ export const KanBanLayout: React.FC = observer(() => {
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members?.map((m) => m.member) ?? null}
|
||||
projects={projects}
|
||||
enableQuickIssueCreate
|
||||
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||
/>
|
||||
) : (
|
||||
<KanBanSwimLanes
|
||||
|
@ -146,13 +146,6 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||
displayProperties={displayProperties}
|
||||
kanBanToggle={kanBanToggle}
|
||||
handleKanBanToggle={handleKanBanToggle}
|
||||
states={states}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate
|
||||
/>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
interface IssueBlockProps {
|
||||
columnId: string;
|
||||
@ -12,14 +12,10 @@ interface IssueBlockProps {
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
display_properties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, estimates } = props;
|
||||
const { columnId, issue, handleIssues, quickActions, display_properties } = props;
|
||||
|
||||
const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
|
||||
handleIssues(group_by, issueToUpdate, "update");
|
||||
@ -54,10 +50,6 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
|
||||
issue={issue}
|
||||
handleIssues={updateIssue}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
{quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { FC } from "react";
|
||||
// components
|
||||
import { IssueBlock } from "components/issues";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types";
|
||||
import { IIssue } from "types";
|
||||
|
||||
interface Props {
|
||||
columnId: string;
|
||||
@ -10,15 +10,10 @@ interface Props {
|
||||
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
display_properties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const IssueBlocksList: FC<Props> = (props) => {
|
||||
const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, estimates } =
|
||||
props;
|
||||
const { columnId, issues, handleIssues, quickActions, display_properties } = props;
|
||||
|
||||
return (
|
||||
<div className="w-full h-full relative divide-y-[0.5px] divide-custom-border-200">
|
||||
@ -31,10 +26,6 @@ export const IssueBlocksList: FC<Props> = (props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
@ -17,14 +17,7 @@ export interface IGroupByList {
|
||||
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
|
||||
display_properties: any;
|
||||
is_list?: boolean;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
projects: IProject[] | null;
|
||||
stateGroups: any;
|
||||
priorities: any;
|
||||
enableQuickIssueCreate?: boolean;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||
@ -37,13 +30,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||
quickActions,
|
||||
display_properties,
|
||||
is_list = false,
|
||||
states,
|
||||
labels,
|
||||
members,
|
||||
projects,
|
||||
stateGroups,
|
||||
priorities,
|
||||
estimates,
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
@ -70,10 +56,6 @@ const GroupByList: React.FC<IGroupByList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
estimates={estimates}
|
||||
/>
|
||||
)}
|
||||
{enableQuickIssueCreate && (
|
||||
@ -121,7 +103,7 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
projects,
|
||||
stateGroups,
|
||||
priorities,
|
||||
estimates,
|
||||
|
||||
enableQuickIssueCreate,
|
||||
} = props;
|
||||
|
||||
@ -137,13 +119,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
is_list
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -157,13 +132,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -177,13 +145,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -197,13 +158,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -217,13 +171,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -237,13 +184,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -257,13 +197,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
@ -277,13 +210,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||
handleIssues={handleIssues}
|
||||
quickActions={quickActions}
|
||||
display_properties={display_properties}
|
||||
states={states}
|
||||
labels={labels}
|
||||
members={members}
|
||||
projects={projects}
|
||||
stateGroups={stateGroups}
|
||||
priorities={priorities}
|
||||
estimates={estimates}
|
||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||
/>
|
||||
)}
|
||||
|
@ -11,21 +11,17 @@ import { IssuePropertyDate } from "../properties/date";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types";
|
||||
import { IIssue, IState, TIssuePriorities } from "types";
|
||||
|
||||
export interface IKanBanProperties {
|
||||
columnId: string;
|
||||
issue: IIssue;
|
||||
handleIssues: (group_by: string | null, issue: IIssue) => void;
|
||||
display_properties: any;
|
||||
states: IState[] | null;
|
||||
labels: IIssueLabels[] | null;
|
||||
members: IUserLite[] | null;
|
||||
estimates: IEstimatePoint[] | null;
|
||||
}
|
||||
|
||||
export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, estimates } = props;
|
||||
const { columnId: group_id, issue, handleIssues, display_properties } = props;
|
||||
|
||||
const handleState = (state: IState) => {
|
||||
handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id });
|
||||
@ -59,13 +55,13 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
<div className="relative flex gap-2 overflow-x-auto whitespace-nowrap">
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
{display_properties && display_properties?.state && states && (
|
||||
{display_properties && display_properties?.state && (
|
||||
<IssuePropertyState
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state_detail || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleState}
|
||||
disabled={false}
|
||||
states={states}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -80,24 +76,24 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
)}
|
||||
|
||||
{/* label */}
|
||||
{display_properties && display_properties?.labels && labels && (
|
||||
{display_properties && display_properties?.labels && (
|
||||
<IssuePropertyLabels
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.labels || null}
|
||||
onChange={handleLabel}
|
||||
labels={labels}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* assignee */}
|
||||
{display_properties && display_properties?.assignee && members && (
|
||||
{display_properties && display_properties?.assignee && (
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow
|
||||
onChange={handleAssignee}
|
||||
disabled={false}
|
||||
members={members}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -124,8 +120,8 @@ export const KanBanProperties: FC<IKanBanProperties> = observer((props) => {
|
||||
{/* estimates */}
|
||||
{display_properties && display_properties?.estimate && (
|
||||
<IssuePropertyEstimates
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.estimate_point || null}
|
||||
estimatePoints={estimates}
|
||||
hideDropdownArrow
|
||||
onChange={handleEstimate}
|
||||
disabled={false}
|
||||
|
@ -1,28 +1,180 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { MembersSelect } from "components/project";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
import { usePopper } from "react-popper";
|
||||
// ui
|
||||
import { AssigneesList, Avatar } from "components/ui";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Check, ChevronDown, Search, User2 } from "lucide-react";
|
||||
// types
|
||||
import { IUserLite } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export interface IIssuePropertyAssignee {
|
||||
value: string[];
|
||||
view?: "profile" | "workspace" | "project";
|
||||
projectId: string | null;
|
||||
value: string[] | string;
|
||||
onChange: (data: string[]) => void;
|
||||
members: IUserLite[] | null;
|
||||
disabled?: boolean;
|
||||
hideDropdownArrow?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
multiple?: true;
|
||||
}
|
||||
|
||||
export const IssuePropertyAssignee: React.FC<IIssuePropertyAssignee> = observer((props) => {
|
||||
const { value, onChange, members, disabled = false, hideDropdownArrow = false } = props;
|
||||
const {
|
||||
view,
|
||||
projectId,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
hideDropdownArrow = false,
|
||||
className,
|
||||
buttonClassName,
|
||||
optionsClassName,
|
||||
placement,
|
||||
multiple = false,
|
||||
} = props;
|
||||
|
||||
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
|
||||
const workspaceSlug = workspaceStore?.workspaceSlug;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const projectMembers = projectId && projectStore?.members?.[projectId];
|
||||
|
||||
const fetchProjectMembers = () =>
|
||||
workspaceSlug && projectId && projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
||||
|
||||
const options = (projectMembers ? projectMembers : [])?.map((member) => ({
|
||||
value: member.member.id,
|
||||
query: member.member.display_name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar user={member.member} />
|
||||
{member.member.display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip
|
||||
tooltipHeading="Assignee"
|
||||
tooltipContent={
|
||||
value && value.length > 0
|
||||
? (projectMembers ? projectMembers : [])
|
||||
?.filter((m) => value.includes(m.member.display_name))
|
||||
.map((m) => m.member.display_name)
|
||||
.join(", ")
|
||||
: "No Assignee"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
<div className="flex items-center cursor-pointer h-full w-full gap-2 text-custom-text-200">
|
||||
{value && value.length > 0 && Array.isArray(value) ? (
|
||||
<AssigneesList userIds={value} length={3} showLength={true} />
|
||||
) : (
|
||||
<span
|
||||
className="flex items-center justify-between gap-1 h-full w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none
|
||||
"
|
||||
>
|
||||
<User2 className="h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const comboboxProps: any = { value, onChange, disabled };
|
||||
if (multiple) comboboxProps.multiple = true;
|
||||
|
||||
return (
|
||||
<MembersSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
members={members ?? undefined}
|
||||
disabled={disabled}
|
||||
hideDropdownArrow={hideDropdownArrow}
|
||||
multiple
|
||||
/>
|
||||
<Combobox as="div" className={`flex-shrink-0 text-left ${className}`} {...comboboxProps}>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => fetchProjectMembers()}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<div
|
||||
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active && !selected ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <Check className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -42,7 +42,7 @@ export const IssuePropertyDate: React.FC<IIssuePropertyDate> = observer((props)
|
||||
<>
|
||||
<Popover.Button
|
||||
ref={dropdownBtn}
|
||||
className={`px-2.5 py-1 h-5 flex items-center rounded border-[0.5px] border-custom-border-300 duration-300 outline-none ${
|
||||
className={`px-2.5 py-1 h-5 flex items-center rounded border-[0.5px] border-custom-border-300 duration-300 outline-none w-full ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
|
@ -1,28 +1,168 @@
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { EstimateSelect } from "components/estimates";
|
||||
|
||||
// hooks
|
||||
import { usePopper } from "react-popper";
|
||||
import useEstimateOption from "hooks/use-estimate-option";
|
||||
// ui
|
||||
import { Check, ChevronDown, Search, Triangle } from "lucide-react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// types
|
||||
import { IEstimatePoint } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
|
||||
export interface IIssuePropertyEstimates {
|
||||
view?: "profile" | "workspace" | "project";
|
||||
projectId: string | null;
|
||||
value: number | null;
|
||||
onChange: (value: number | null) => void;
|
||||
estimatePoints: IEstimatePoint[] | null;
|
||||
disabled?: boolean;
|
||||
hideDropdownArrow?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
export const IssuePropertyEstimates: React.FC<IIssuePropertyEstimates> = observer((props) => {
|
||||
const { value, onChange, estimatePoints, disabled, hideDropdownArrow = false } = props;
|
||||
const {
|
||||
view,
|
||||
projectId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
hideDropdownArrow = false,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
optionsClassName = "",
|
||||
placement,
|
||||
} = props;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const options: { value: number | null; query: string; content: any }[] | undefined = (estimatePoints ?? []).map(
|
||||
(estimate) => ({
|
||||
value: estimate.key,
|
||||
query: estimate.value,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
{estimate.value}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
);
|
||||
options?.unshift({
|
||||
value: null,
|
||||
query: "none",
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
None
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const selectedEstimate = estimatePoints?.find((e) => e.key === value);
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="Estimate" tooltipContent={selectedEstimate?.value ?? "None"} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
<Triangle className="h-3 w-3" strokeWidth={2} />
|
||||
<span className="truncate">{selectedEstimate?.value ?? "None"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<EstimateSelect
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
estimatePoints={estimatePoints ?? undefined}
|
||||
buttonClassName="h-5"
|
||||
onChange={(val) => onChange(val as number | null)}
|
||||
disabled={disabled}
|
||||
hideDropdownArrow={hideDropdownArrow}
|
||||
/>
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<div
|
||||
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <Check className="h-3.5 w-3.5" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
6
web/components/issues/issue-layouts/properties/index.tsx
Normal file
6
web/components/issues/issue-layouts/properties/index.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./assignee";
|
||||
export * from "./date";
|
||||
export * from "./estimates";
|
||||
export * from "./labels";
|
||||
export * from "./priority";
|
||||
export * from "./state";
|
@ -1,28 +1,212 @@
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// hooks
|
||||
import { usePopper } from "react-popper";
|
||||
// components
|
||||
import { LabelSelect } from "components/labels";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// types
|
||||
import { IIssueLabels } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export interface IIssuePropertyLabels {
|
||||
view?: "profile" | "workspace" | "project";
|
||||
projectId: string | null;
|
||||
value: string[];
|
||||
onChange: (data: string[]) => void;
|
||||
labels: IIssueLabels[] | null;
|
||||
disabled?: boolean;
|
||||
hideDropdownArrow?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
maxRender?: number;
|
||||
}
|
||||
|
||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||
const { value, onChange, labels, disabled, hideDropdownArrow = false } = props;
|
||||
const {
|
||||
view,
|
||||
projectId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
hideDropdownArrow = false,
|
||||
className,
|
||||
buttonClassName,
|
||||
optionsClassName,
|
||||
placement,
|
||||
maxRender = 2,
|
||||
} = props;
|
||||
|
||||
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
|
||||
const workspaceSlug = workspaceStore?.workspaceSlug;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const projectLabels = projectId && projectStore?.labels?.[projectId];
|
||||
|
||||
const fetchProjectLabels = () =>
|
||||
workspaceSlug && projectId && projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
||||
|
||||
const options = (projectLabels ? projectLabels : []).map((label) => ({
|
||||
value: label.id,
|
||||
query: label.name,
|
||||
content: (
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color,
|
||||
}}
|
||||
/>
|
||||
<span>{label.name}</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const filteredOptions =
|
||||
query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<LabelSelect
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
labels={labels ?? undefined}
|
||||
buttonClassName="h-5"
|
||||
disabled={disabled}
|
||||
hideDropdownArrow={hideDropdownArrow}
|
||||
/>
|
||||
multiple
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
? "cursor-pointer"
|
||||
: "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => fetchProjectLabels()}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200 h-full">
|
||||
{value.length > 0 ? (
|
||||
value.length <= maxRender ? (
|
||||
<>
|
||||
{(projectLabels ? projectLabels : [])
|
||||
?.filter((l) => value.includes(l.id))
|
||||
.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="flex cursor-default items-center flex-shrink-0 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs h-full"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-custom-text-200">
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color ?? "#000000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full flex cursor-default items-center flex-shrink-0 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs">
|
||||
<Tooltip
|
||||
position="top"
|
||||
tooltipHeading="Labels"
|
||||
tooltipContent={(projectLabels ? projectLabels : [])
|
||||
?.filter((l) => value.includes(l.id))
|
||||
.map((l) => l.name)
|
||||
.join(", ")}
|
||||
>
|
||||
<div className="h-full flex items-center gap-1.5 text-custom-text-200">
|
||||
<span className="h-2 w-2 flex-shrink-0 rounded-full bg-custom-primary" />
|
||||
{`${value.length} Labels`}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-xs rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 hover:bg-custom-background-80">
|
||||
Select labels
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
|
||||
<Combobox.Options>
|
||||
<div
|
||||
className={`z-10 border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <Check className={`h-3.5 w-3.5`} />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
);
|
||||
});
|
||||
|
@ -1,28 +1,175 @@
|
||||
import { Fragment, useState } from "react";
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { StateSelect } from "components/states";
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
|
||||
// hooks
|
||||
import { usePopper } from "react-popper";
|
||||
// ui
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { StateGroupIcon, Tooltip } from "@plane/ui";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
// types
|
||||
import { IState } from "types";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { RootStore } from "store/root";
|
||||
|
||||
export interface IIssuePropertyState {
|
||||
view?: "profile" | "workspace" | "project";
|
||||
projectId: string | null;
|
||||
value: IState;
|
||||
onChange: (state: IState) => void;
|
||||
states: IState[] | null;
|
||||
disabled?: boolean;
|
||||
hideDropdownArrow?: boolean;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
optionsClassName?: string;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
export const IssuePropertyState: React.FC<IIssuePropertyState> = observer((props) => {
|
||||
const { value, onChange, states, disabled, hideDropdownArrow = false } = props;
|
||||
const {
|
||||
view,
|
||||
projectId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
hideDropdownArrow = false,
|
||||
className,
|
||||
buttonClassName,
|
||||
optionsClassName,
|
||||
placement,
|
||||
} = props;
|
||||
|
||||
const { workspace: workspaceStore, project: projectStore }: RootStore = useMobxStore();
|
||||
const workspaceSlug = workspaceStore?.workspaceSlug;
|
||||
|
||||
const [query, setQuery] = useState("");
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const projectStates: IState[] = [];
|
||||
const projectStatesByGroup = projectId && projectStore?.states?.[projectId];
|
||||
if (projectStatesByGroup)
|
||||
for (const group in projectStatesByGroup) projectStates.push(...projectStatesByGroup[group]);
|
||||
|
||||
const fetchProjectStates = () =>
|
||||
workspaceSlug && projectId && projectStore.fetchProjectStates(workspaceSlug, projectId);
|
||||
|
||||
const dropdownOptions = projectStates?.map((state) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={state.group} color={state.color} />
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
options: {
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const filteredOptions =
|
||||
query === ""
|
||||
? dropdownOptions
|
||||
: dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase()));
|
||||
|
||||
const label = (
|
||||
<Tooltip tooltipHeading="State" tooltipContent={value?.name ?? ""} position="top">
|
||||
<div className="flex items-center cursor-pointer w-full gap-2 text-custom-text-200">
|
||||
{value && <StateGroupIcon stateGroup={value.group} color={value.color} />}
|
||||
<span className="truncate">{value?.name ?? "State"}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<StateSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
states={states ?? undefined}
|
||||
buttonClassName="h-5"
|
||||
disabled={disabled}
|
||||
hideDropdownArrow={hideDropdownArrow}
|
||||
/>
|
||||
<>
|
||||
{workspaceSlug && projectId && (
|
||||
<Combobox
|
||||
as="div"
|
||||
className={`flex-shrink-0 text-left ${className}`}
|
||||
value={value.id}
|
||||
onChange={(data: string) => {
|
||||
const selectedState = projectStates?.find((state) => state.id === data);
|
||||
if (selectedState) onChange(selectedState);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex items-center justify-between gap-1 w-full text-xs px-2.5 py-1 rounded border-[0.5px] border-custom-border-300 duration-300 focus:outline-none ${
|
||||
disabled ? "cursor-not-allowed text-custom-text-200" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
onClick={() => fetchProjectStates()}
|
||||
>
|
||||
{label}
|
||||
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
|
||||
</button>
|
||||
</Combobox.Button>
|
||||
<Combobox.Options className="fixed z-10">
|
||||
<div
|
||||
className={`border border-custom-border-300 px-2 py-2.5 rounded bg-custom-background-100 text-xs shadow-custom-shadow-rg focus:outline-none w-48 whitespace-nowrap my-1 ${optionsClassName}`}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<div className="flex w-full items-center justify-start rounded border border-custom-border-200 bg-custom-background-90 px-2">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-300" />
|
||||
<Combobox.Input
|
||||
className="w-full bg-transparent py-1 px-2 text-xs text-custom-text-200 placeholder:text-custom-text-400 focus:outline-none"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 space-y-1 max-h-48 overflow-y-scroll`}>
|
||||
{filteredOptions ? (
|
||||
filteredOptions.length > 0 ? (
|
||||
filteredOptions.map((option) => (
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, selected }) =>
|
||||
`flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${
|
||||
active ? "bg-custom-background-80" : ""
|
||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{option.content}
|
||||
{selected && <Check className="h-3.5 w-3.5" />}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
) : (
|
||||
<span className="flex items-center gap-2 p-1">
|
||||
<p className="text-left text-custom-text-200 ">No matching results</p>
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<p className="text-center text-custom-text-200">Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { MembersSelect } from "components/project";
|
||||
import { IssuePropertyAssignee } from "../../properties";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
@ -21,11 +21,11 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-full px-4">
|
||||
<MembersSelect
|
||||
<>
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue.project_detail.id ?? null}
|
||||
value={issue.assignees}
|
||||
onChange={(data) => onChange({ assignees: data })}
|
||||
members={members ?? []}
|
||||
buttonClassName="!p-0 !rounded-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
@ -46,6 +46,6 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = ({ issue, members, onC
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
// components
|
||||
import { EstimateSelect } from "components/estimates";
|
||||
import { IssuePropertyEstimates } from "../../properties";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
@ -21,14 +21,12 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<EstimateSelect
|
||||
<IssuePropertyEstimates
|
||||
projectId={issue.project_detail.id ?? null}
|
||||
value={issue.estimate_point}
|
||||
onChange={(data) => onChange({ estimate_point: data })}
|
||||
className="h-full"
|
||||
buttonClassName="!border-0 !h-full !w-full !rounded-none px-4"
|
||||
estimatePoints={undefined}
|
||||
disabled={disabled}
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{isExpanded &&
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { LabelSelect } from "components/labels";
|
||||
import { IssuePropertyLabels } from "../../properties";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// types
|
||||
@ -24,10 +24,10 @@ export const SpreadsheetLabelColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<LabelSelect
|
||||
<IssuePropertyLabels
|
||||
projectId={issue.project_detail.id ?? null}
|
||||
value={issue.labels}
|
||||
onChange={(data) => onChange({ labels: data })}
|
||||
labels={labels ?? []}
|
||||
className="h-full"
|
||||
buttonClassName="!border-0 !h-full !w-full !rounded-none"
|
||||
hideDropdownArrow
|
||||
|
@ -1,11 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
// components
|
||||
import { StateSelect } from "components/states";
|
||||
import { IssuePropertyState } from "../../properties";
|
||||
// hooks
|
||||
import useSubIssue from "hooks/use-sub-issue";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
// types
|
||||
import { IIssue, IStateResponse } from "types";
|
||||
|
||||
@ -24,16 +22,13 @@ export const SpreadsheetStateColumn: React.FC<Props> = (props) => {
|
||||
|
||||
const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded);
|
||||
|
||||
const statesList = getStatesList(states);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StateSelect
|
||||
<IssuePropertyState
|
||||
projectId={issue.project_detail.id ?? null}
|
||||
value={issue.state_detail}
|
||||
onChange={(data) => onChange({ state: data.id, state_detail: data })}
|
||||
states={statesList}
|
||||
className="h-full"
|
||||
buttonClassName="!border-0 !h-full !w-full !rounded-none"
|
||||
buttonClassName="!shadow-none !border-0"
|
||||
hideDropdownArrow
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
@ -83,7 +83,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
||||
ref={containerRef}
|
||||
className="flex max-h-full h-full overflow-y-auto divide-x-[0.5px] divide-custom-border-200"
|
||||
>
|
||||
{issues ? (
|
||||
{issues && issues.length > 0 ? (
|
||||
<>
|
||||
<div className="sticky left-0 w-[28rem] z-[2]">
|
||||
<div
|
||||
|
@ -34,8 +34,6 @@ export const IssueActivityCard: FC<IssueActivityCard> = (props) => {
|
||||
issueCommentReactionRemove,
|
||||
} = props;
|
||||
|
||||
console.log("issueComments", issueComments);
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-4">
|
||||
|
@ -36,8 +36,8 @@ export const IssueComment: FC<IIssueComment> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="font-medium text-xl">Activity</div>
|
||||
<div className="flex flex-col gap-3 border-t py-6 border-custom-border-200">
|
||||
<div className="font-medium text-lg">Activity</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<IssueCommentEditor
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { FC, useCallback, useEffect, useState } from "react";
|
||||
import { ChangeEvent, FC, useCallback, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// packages
|
||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||
// components
|
||||
import { TextArea } from "@plane/ui";
|
||||
import { IssueReaction } from "./reactions";
|
||||
// hooks
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
|
||||
const fileService = new FileService();
|
||||
|
||||
@ -26,24 +27,45 @@ interface IPeekOverviewIssueDetails {
|
||||
|
||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
||||
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||
|
||||
const { handleSubmit, watch, reset, control } = useForm<IIssue>({
|
||||
const [characterLimit, setCharacterLimit] = useState(false);
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
reset,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<IIssue>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description_html: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { setShowAlert } = useReloadConfirmations();
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (!issue) return;
|
||||
await issueUpdate({
|
||||
...issue,
|
||||
name: formData.name ?? "",
|
||||
description_html: formData.description_html ?? "<p></p>",
|
||||
});
|
||||
},
|
||||
[issue, issueUpdate]
|
||||
);
|
||||
|
||||
reset({
|
||||
...issue,
|
||||
});
|
||||
}, [issue, reset]);
|
||||
const debouncedIssueDescription = useDebouncedCallback(async (_data: any) => {
|
||||
issueUpdate({ ...issue, description_html: _data });
|
||||
}, 1500);
|
||||
|
||||
const debouncedTitleSave = useDebouncedCallback(async () => {
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||
}, 1500);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitting === "submitted") {
|
||||
@ -56,62 +78,80 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
||||
}
|
||||
}, [isSubmitting, setShowAlert]);
|
||||
|
||||
const handleDescriptionFormSubmit = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||
// reset form values
|
||||
useEffect(() => {
|
||||
if (!issue) return;
|
||||
|
||||
issueUpdate({ name: formData.name ?? "", description_html: formData.description_html });
|
||||
},
|
||||
[issueUpdate]
|
||||
);
|
||||
|
||||
const debouncedIssueFormSave = useDebouncedCallback(async () => {
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting("submitted"));
|
||||
}, 1500);
|
||||
reset({
|
||||
...issue,
|
||||
});
|
||||
}, [issue, reset]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="font-medium text-sm text-custom-text-200">
|
||||
<>
|
||||
<span className="font-medium text-base text-custom-text-400">
|
||||
{issue?.project_detail?.identifier}-{issue?.sequence_id}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div className="font-medium text-xl">{watch("name")}</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
{true ? (
|
||||
<Controller
|
||||
name="description_html"
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RichTextEditor
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
<TextArea
|
||||
id="name"
|
||||
name="name"
|
||||
value={value}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
placeholder="Enter issue name"
|
||||
onFocus={() => setCharacterLimit(true)}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCharacterLimit(false);
|
||||
setIsSubmitting("submitting");
|
||||
onChange(description_html);
|
||||
debouncedIssueFormSave();
|
||||
debouncedTitleSave();
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
customClassName="p-3 min-h-[80px] shadow-sm"
|
||||
required={true}
|
||||
className="min-h-10 block w-full resize-none overflow-hidden rounded border-none bg-transparent text-xl outline-none ring-0 focus:ring-1 focus:ring-custom-primary !p-0 focus:!px-3 focus:!py-2"
|
||||
hasError={Boolean(errors?.description)}
|
||||
role="textbox"
|
||||
disabled={!true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
||||
) : (
|
||||
<h4 className="break-words text-2xl font-semibold">{issue.name}</h4>
|
||||
)}
|
||||
{characterLimit && true && (
|
||||
<div className="pointer-events-none absolute bottom-1 right-1 z-[2] rounded bg-custom-background-100 text-custom-text-200 p-0.5 text-xs">
|
||||
<span className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : ""}`}>
|
||||
{watch("name").length}
|
||||
</span>
|
||||
/255
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IssueReaction
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span>{errors.name ? errors.name.message : null}</span>
|
||||
|
||||
<span className="text-black">
|
||||
<RichTextEditor
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
value={issue?.description_html}
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
debouncedIssueDescription(description_html);
|
||||
}}
|
||||
customClassName="mt-0"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<IssueReaction
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,139 +1,358 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// ui icons
|
||||
import { DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { CalendarDays, Signal } from "lucide-react";
|
||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { CalendarDays, ContrastIcon, Link2, Plus, Signal, Tag, Triangle, User2 } from "lucide-react";
|
||||
import {
|
||||
SidebarAssigneeSelect,
|
||||
SidebarCycleSelect,
|
||||
SidebarEstimateSelect,
|
||||
SidebarLabelSelect,
|
||||
SidebarModuleSelect,
|
||||
SidebarParentSelect,
|
||||
SidebarPrioritySelect,
|
||||
SidebarStateSelect,
|
||||
} from "../sidebar-select";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
|
||||
// components
|
||||
import { IssuePropertyState } from "components/issues/issue-layouts/properties/state";
|
||||
import { IssuePropertyPriority } from "components/issues/issue-layouts/properties/priority";
|
||||
import { IssuePropertyAssignee } from "components/issues/issue-layouts/properties/assignee";
|
||||
import { IssuePropertyDate } from "components/issues/issue-layouts/properties/date";
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { LinkModal, LinksList } from "components/core";
|
||||
// types
|
||||
import { IIssue, IState, IUserLite, TIssuePriorities } from "types";
|
||||
import { ICycle, IIssue, IIssueLink, IModule, TIssuePriorities, linkDetails } from "types";
|
||||
// contexts
|
||||
import { useProjectMyMembership } from "contexts/project-member.context";
|
||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
// services
|
||||
import { IssueService } from "services/issue";
|
||||
|
||||
interface IPeekOverviewProperties {
|
||||
issue: IIssue;
|
||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||
states: IState[] | null;
|
||||
members: IUserLite[] | null;
|
||||
priorities: any;
|
||||
user: any;
|
||||
}
|
||||
|
||||
const issueService = new IssueService();
|
||||
|
||||
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = (props) => {
|
||||
const { issue, issueUpdate, states, members, priorities } = props;
|
||||
const { issue, issueUpdate, user } = props;
|
||||
const [linkModal, setLinkModal] = useState(false);
|
||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||
|
||||
const handleState = (_state: IState) => {
|
||||
issueUpdate({ ...issue, state: _state.id });
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
|
||||
const handleState = (_state: string) => {
|
||||
issueUpdate({ ...issue, state: _state });
|
||||
};
|
||||
|
||||
const handlePriority = (_priority: TIssuePriorities) => {
|
||||
issueUpdate({ ...issue, priority: _priority });
|
||||
};
|
||||
|
||||
const handleAssignee = (_assignees: string[]) => {
|
||||
issueUpdate({ ...issue, assignees: _assignees });
|
||||
};
|
||||
|
||||
const handleStartDate = (_startDate: string) => {
|
||||
const handleEstimate = (_estimate: number | null) => {
|
||||
issueUpdate({ ...issue, estimate_point: _estimate });
|
||||
};
|
||||
const handleStartDate = (_startDate: string | null) => {
|
||||
issueUpdate({ ...issue, start_date: _startDate });
|
||||
};
|
||||
|
||||
const handleTargetDate = (_targetDate: string) => {
|
||||
const handleTargetDate = (_targetDate: string | null) => {
|
||||
issueUpdate({ ...issue, target_date: _targetDate });
|
||||
};
|
||||
const handleParent = (_parent: string) => {
|
||||
issueUpdate({ ...issue, parent: _parent });
|
||||
};
|
||||
const handleCycle = (_cycle: ICycle) => {
|
||||
issueUpdate({ ...issue, cycle: _cycle.id });
|
||||
};
|
||||
const handleModule = (_module: IModule) => {
|
||||
issueUpdate({ ...issue, module: _module.id });
|
||||
};
|
||||
const handleLabels = (formData: Partial<IIssue>) => {
|
||||
issueUpdate({ ...issue, ...formData });
|
||||
};
|
||||
|
||||
const handleCreateLink = async (formData: IIssueLink) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
await issueService
|
||||
.createIssueLink(workspaceSlug as string, projectId as string, issue.id, payload)
|
||||
.then(() => mutate(ISSUE_DETAILS(issue.id)))
|
||||
.catch((err) => {
|
||||
if (err.status === 400)
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "This URL already exists for this issue.",
|
||||
});
|
||||
else
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateLink = async (formData: IIssueLink, linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const payload = { metadata: {}, ...formData };
|
||||
|
||||
const updatedLinks = issue.issue_link.map((l) =>
|
||||
l.id === linkId
|
||||
? {
|
||||
...l,
|
||||
title: formData.title,
|
||||
url: formData.url,
|
||||
}
|
||||
: l
|
||||
);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.updateIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId, payload)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditLink = (link: linkDetails) => {
|
||||
setSelectedLinkToUpdate(link);
|
||||
setLinkModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (!workspaceSlug || !projectId || !issue) return;
|
||||
|
||||
const updatedLinks = issue.issue_link.filter((l) => l.id !== linkId);
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(issue.id),
|
||||
(prevData) => ({ ...(prevData as IIssue), issue_link: updatedLinks }),
|
||||
false
|
||||
);
|
||||
|
||||
await issueService
|
||||
.deleteIssueLink(workspaceSlug as string, projectId as string, issue.id, linkId)
|
||||
.then(() => {
|
||||
mutate(ISSUE_DETAILS(issue.id));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const isNotAllowed = user?.memberRole?.isGuest || user?.memberRole?.isViewer;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* state */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
|
||||
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
|
||||
<DoubleCircleIcon className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<>
|
||||
<LinkModal
|
||||
isOpen={linkModal}
|
||||
handleClose={() => {
|
||||
setLinkModal(false);
|
||||
setSelectedLinkToUpdate(null);
|
||||
}}
|
||||
data={selectedLinkToUpdate}
|
||||
status={selectedLinkToUpdate ? true : false}
|
||||
createIssueLink={handleCreateLink}
|
||||
updateIssueLink={handleUpdateLink}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-5 py-5 w-full">
|
||||
{/* state */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarStateSelect value={issue?.state || ""} onChange={handleState} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-custom-text-200 line-clamp-1">State</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<IssuePropertyState
|
||||
value={issue?.state_detail || null}
|
||||
onChange={handleState}
|
||||
states={states}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* assignees */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
|
||||
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
|
||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
||||
{/* assignee */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarAssigneeSelect value={issue.assignees || []} onChange={handleAssignee} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-custom-text-200 line-clamp-1">Assignees</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<IssuePropertyAssignee
|
||||
value={issue?.assignees || null}
|
||||
onChange={(ids: string[]) => handleAssignee(ids)}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
members={members}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* priority */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
|
||||
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
|
||||
<Signal className="h-3.5 w-3.5" />
|
||||
{/* priority */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarPrioritySelect value={issue.priority || ""} onChange={handlePriority} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-custom-text-200 line-clamp-1">Priority</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<IssuePropertyPriority
|
||||
value={issue?.priority || null}
|
||||
onChange={handlePriority}
|
||||
disabled={false}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* start_date */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
|
||||
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
|
||||
<CalendarDays className="h-3.5 w-3.5" />
|
||||
{/* estimate */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
||||
<p>Estimate</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarEstimateSelect value={issue.estimate_point} onChange={handleEstimate} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-custom-text-200 line-clamp-1">Start date</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<IssuePropertyDate
|
||||
value={issue?.start_date || null}
|
||||
onChange={(date: string) => handleStartDate(date)}
|
||||
disabled={false}
|
||||
placeHolder={`Start date`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* target_date */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-shrink-0 flex items-center gap-2 w-48 whitespace-nowrap">
|
||||
<div className="w-4 h-4 flex justify-center items-center overflow-hidden">
|
||||
<CalendarDays className="h-3.5 w-3.5" />
|
||||
{/* start date */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Start date</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomDatePicker
|
||||
placeholder="Start date"
|
||||
value={issue.start_date}
|
||||
onChange={handleStartDate}
|
||||
className="bg-custom-background-80 border-none !px-2.5 !py-0.5"
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* due date */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date}
|
||||
onChange={handleTargetDate}
|
||||
className="bg-custom-background-80 border-none !px-2.5 !py-0.5"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* parent */}
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<User2 className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Parent</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-medium text-custom-text-200 line-clamp-1">Target date</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<IssuePropertyDate
|
||||
value={issue?.target_date || null}
|
||||
onChange={(date: string) => handleTargetDate(date)}
|
||||
disabled={false}
|
||||
placeHolder={`Target date`}
|
||||
/>
|
||||
|
||||
<span className="border-t border-custom-border-200" />
|
||||
|
||||
<div className="flex flex-col gap-5 py-5 w-full">
|
||||
<div className="flex items-center gap-2 w-80">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Cycle</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarCycleSelect issueDetail={issue} handleCycleChange={handleCycle} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-80">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Module</p>
|
||||
</div>
|
||||
<div>
|
||||
<SidebarModuleSelect issueDetail={issue} handleModuleChange={handleModule} disabled={isNotAllowed} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-40 flex-shrink-0">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<SidebarLabelSelect
|
||||
issueDetails={issue}
|
||||
labelList={issue.labels}
|
||||
submitChanges={handleLabels}
|
||||
isNotAllowed={isNotAllowed}
|
||||
uneditable={isNotAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="border-t border-custom-border-200" />
|
||||
|
||||
<div className="flex flex-col gap-5 pt-5 w-full">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2 w-80">
|
||||
<div className="flex items-center gap-2 w-40">
|
||||
<Link2 className="h-4 w-4 rotate-45 flex-shrink-0" />
|
||||
<p>Links</p>
|
||||
</div>
|
||||
<div>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex ${
|
||||
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
|
||||
onClick={() => setLinkModal(true)}
|
||||
disabled={false}
|
||||
>
|
||||
<Plus className="h-3 w-3" /> New
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{issue?.issue_link && issue.issue_link.length > 0 ? (
|
||||
<LinksList
|
||||
links={issue.issue_link}
|
||||
handleDeleteLink={handleDeleteLink}
|
||||
handleEditLink={handleEditLink}
|
||||
userAuth={memberRole}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -26,13 +26,13 @@ export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
|
||||
type="button"
|
||||
onClick={() => handleReaction(reaction)}
|
||||
key={reaction}
|
||||
className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded-md ${
|
||||
className={`flex items-center gap-1.5 text-custom-text-100 text-sm h-full px-2 py-1 rounded ${
|
||||
isUserReacted(issueReactions[reaction])
|
||||
? `bg-custom-primary-100/20 hover:bg-custom-primary-100/30`
|
||||
? `bg-custom-primary-100/10 hover:bg-custom-primary-100/30`
|
||||
: `bg-custom-background-90 hover:bg-custom-background-100/30`
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span className="text-sm">{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={`${
|
||||
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : ``
|
||||
|
@ -23,15 +23,11 @@ export const IssueReactionSelector: FC<IIssueReactionSelector> = (props) => {
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`${
|
||||
open ? "" : "bg-custom-background-90"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-90 focus:outline-none transition-all hover:bg-custom-background-100`}
|
||||
open ? "" : "bg-custom-background-80"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-80 focus:outline-none transition-all hover:bg-custom-background-90`}
|
||||
>
|
||||
<span
|
||||
className={`flex justify-center items-center rounded-md px-2 ${
|
||||
size === "sm" ? "w-6 h-6" : size === "md" ? "w-7 h-7" : "w-8 h-8"
|
||||
}`}
|
||||
>
|
||||
<SmilePlus className="text-custom-text-100 h-3.5 w-3.5" />
|
||||
<span className={`flex justify-center items-center rounded px-2 py-1.5`}>
|
||||
<SmilePlus className={`${size === "sm" ? "w-3 h-3" : size === "md" ? "w-3.5 h-3.5" : "w-4 h-4"}`} />
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
|
@ -7,8 +7,6 @@ import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { RootStore } from "store/root";
|
||||
// constants
|
||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||
|
||||
interface IIssuePeekOverview {
|
||||
workspaceSlug: string;
|
||||
@ -21,11 +19,7 @@ interface IIssuePeekOverview {
|
||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, handleIssue, children } = props;
|
||||
|
||||
const { project: projectStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const states = projectStore?.projectStates || undefined;
|
||||
const members = projectStore?.projectMembers || undefined;
|
||||
const priorities = ISSUE_PRIORITIES || undefined;
|
||||
const { issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const issueUpdate = (_data: Partial<IIssue>) => {
|
||||
if (handleIssue) {
|
||||
@ -55,14 +49,17 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
const issueCommentReactionRemove = (commentId: string, reaction: string) =>
|
||||
issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction);
|
||||
|
||||
const issueSubscriptionCreate = () => issueDetailStore.createIssueSubscription(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueSubscriptionRemove = () => issueDetailStore.removeIssueSubscription(workspaceSlug, projectId, issueId);
|
||||
|
||||
const handleDeleteIssue = () => issueDetailStore.deleteIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
return (
|
||||
<IssueView
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
states={states}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
issueUpdate={issueUpdate}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
@ -71,6 +68,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
issueSubscriptionCreate={issueSubscriptionCreate}
|
||||
issueSubscriptionRemove={issueSubscriptionRemove}
|
||||
handleDeleteIssue={handleDeleteIssue}
|
||||
>
|
||||
{children}
|
||||
</IssueView>
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { FC, ReactNode, useEffect, useState } from "react";
|
||||
import { FC, ReactNode, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Maximize2, ArrowRight, Link, Trash, PanelRightOpen, Square, SquareCode } from "lucide-react";
|
||||
import { PanelRightOpen, Square, SquareCode, MoveRight, MoveDiagonal, Bell, Link2, Trash2 } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
// components
|
||||
import { PeekOverviewIssueDetails } from "./issue-detail";
|
||||
import { PeekOverviewProperties } from "./properties";
|
||||
import { IssueComment } from "./activity";
|
||||
import { Button, CustomSelect, FullScreenPeekIcon, ModalPeekIcon, SidePeekIcon } from "@plane/ui";
|
||||
import { DeleteIssueModal } from "../delete-issue-modal";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import { RootStore } from "store/root";
|
||||
// hooks
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
import useToast from "hooks/use-toast";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
|
||||
interface IIssueView {
|
||||
workspaceSlug: string;
|
||||
@ -25,9 +30,9 @@ interface IIssueView {
|
||||
issueCommentRemove: (commentId: string) => void;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
states: any;
|
||||
members: any;
|
||||
priorities: any;
|
||||
issueSubscriptionCreate: () => void;
|
||||
issueSubscriptionRemove: () => void;
|
||||
handleDeleteIssue: () => Promise<void>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@ -36,17 +41,17 @@ type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
const peekOptions: { key: TPeekModes; icon: any; title: string }[] = [
|
||||
{
|
||||
key: "side-peek",
|
||||
icon: PanelRightOpen,
|
||||
icon: SidePeekIcon,
|
||||
title: "Side Peek",
|
||||
},
|
||||
{
|
||||
key: "modal",
|
||||
icon: Square,
|
||||
icon: ModalPeekIcon,
|
||||
title: "Modal",
|
||||
},
|
||||
{
|
||||
key: "full-screen",
|
||||
icon: SquareCode,
|
||||
icon: FullScreenPeekIcon,
|
||||
title: "Full Screen",
|
||||
},
|
||||
];
|
||||
@ -64,9 +69,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issueCommentRemove,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
states,
|
||||
members,
|
||||
priorities,
|
||||
issueSubscriptionCreate,
|
||||
issueSubscriptionRemove,
|
||||
handleDeleteIssue,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
@ -76,8 +81,20 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
const { user: userStore, issueDetail: issueDetailStore }: RootStore = useMobxStore();
|
||||
|
||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||
const handlePeekMode = (_peek: TPeekModes) => {
|
||||
if (peekMode != _peek) setPeekMode(_peek);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues/${peekIssueId}`).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
message: "Issue link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateRoutePeekId = () => {
|
||||
@ -117,128 +134,129 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
}
|
||||
);
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId
|
||||
? `ISSUE_PEEK_OVERVIEW_SUBSCRIPTION_${workspaceSlug}_${projectId}_${peekIssueId}`
|
||||
: null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && issueId && peekIssueId && issueId === peekIssueId) {
|
||||
await issueDetailStore.fetchIssueSubscription(workspaceSlug, projectId, issueId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const issue = issueDetailStore.getIssue;
|
||||
const issueReactions = issueDetailStore.getIssueReactions;
|
||||
const issueComments = issueDetailStore.getIssueComments;
|
||||
const issueSubscription = issueDetailStore.getIssueSubscription;
|
||||
|
||||
const user = userStore?.currentUser;
|
||||
|
||||
return (
|
||||
<div className="w-full !text-base">
|
||||
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
|
||||
{children}
|
||||
</div>
|
||||
const currentMode = peekOptions.find((m) => m.key === peekMode);
|
||||
|
||||
{issueId === peekIssueId && (
|
||||
<div
|
||||
className={`fixed z-50 overflow-hidden bg-custom-background-80 flex flex-col transition-all duration-300 border border-custom-border-200 rounded shadow-custom-shadow-2xl
|
||||
return (
|
||||
<>
|
||||
{issue && (
|
||||
<DeleteIssueModal
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
data={issue}
|
||||
onSubmit={handleDeleteIssue}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full !text-base">
|
||||
<div onClick={updateRoutePeekId} className="w-full cursor-pointer">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{issueId === peekIssueId && (
|
||||
<div
|
||||
className={`fixed z-20 overflow-hidden bg-custom-background-100 flex flex-col transition-all duration-300 border border-custom-border-200 rounded
|
||||
${peekMode === "side-peek" ? `w-full md:w-[50%] top-0 right-0 bottom-0` : ``}
|
||||
${peekMode === "modal" ? `top-[50%] left-[50%] -translate-x-[50%] -translate-y-[50%] w-5/6 h-5/6` : ``}
|
||||
${peekMode === "full-screen" ? `top-0 right-0 bottom-0 left-0 m-4` : ``}
|
||||
`}
|
||||
>
|
||||
{/* header */}
|
||||
<div className="flex-shrink-0 w-full p-4 py-3 relative flex items-center gap-2 border-b border-custom-border-200">
|
||||
<div
|
||||
className="flex-shrink-0 overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100"
|
||||
onClick={removeRoutePeekId}
|
||||
>
|
||||
<ArrowRight width={12} strokeWidth={2} />
|
||||
</div>
|
||||
style={{
|
||||
boxShadow:
|
||||
"0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12)",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div className="relative flex items-center justify-between p-5 border-b border-custom-border-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={removeRoutePeekId}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="flex-shrink-0 overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100"
|
||||
onClick={redirectToIssueDetail}
|
||||
>
|
||||
<Maximize2 width={12} strokeWidth={2} />
|
||||
</div>
|
||||
<button onClick={redirectToIssueDetail}>
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{currentMode && (
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
<CustomSelect
|
||||
value={currentMode}
|
||||
onChange={(val: any) => setPeekMode(val)}
|
||||
customButton={
|
||||
<button type="button" className="">
|
||||
<currentMode.icon className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{peekOptions.map((mode) => (
|
||||
<CustomSelect.Option key={mode.key} value={mode.key}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<mode.icon className={`h-4 w-4 flex-shrink-0 -my-1 `} />
|
||||
{mode.title}
|
||||
</div>
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 flex items-center gap-2">
|
||||
{peekOptions.map((_option) => (
|
||||
<div
|
||||
key={_option?.key}
|
||||
className={`px-1.5 min-w-6 h-6 flex justify-center items-center gap-1 rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100
|
||||
${peekMode === _option?.key ? `bg-custom-background-100` : ``}
|
||||
`}
|
||||
onClick={() => handlePeekMode(_option?.key)}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
size="sm"
|
||||
prependIcon={<Bell className="h-3 w-3" />}
|
||||
variant="outline-primary"
|
||||
onClick={() =>
|
||||
issueSubscription && issueSubscription.subscribed
|
||||
? issueSubscriptionRemove
|
||||
: issueSubscriptionCreate
|
||||
}
|
||||
>
|
||||
<_option.icon width={14} strokeWidth={2} />
|
||||
<div className="text-xs font-medium">{_option?.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex justify-end items-center gap-2">
|
||||
<div className="px-1.5 min-w-6 h-6 text-xs font-medium flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
|
||||
Subscribe
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
|
||||
<Link width={12} strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden w-6 h-6 flex justify-center items-center rounded-sm transition-all duration-100 border border-custom-border-200 cursor-pointer hover:bg-custom-background-100">
|
||||
<Trash width={12} strokeWidth={2} />
|
||||
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||
</Button>
|
||||
<button onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200 -rotate-45" />
|
||||
</button>
|
||||
<button onClick={() => setDeleteIssueModal(true)}>
|
||||
<Trash2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
{issueDetailStore?.loader && !issue ? (
|
||||
<div className="text-center py-10">Loading...</div>
|
||||
) : (
|
||||
issue && (
|
||||
<>
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="space-y-6 p-4 py-5">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
states={states}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
/>
|
||||
|
||||
<div className="border-t border-custom-border-400" />
|
||||
|
||||
<IssueComment
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
issueComments={issueComments}
|
||||
issueCommentCreate={issueCommentCreate}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex">
|
||||
<div className="w-full h-full space-y-6 p-4 py-5">
|
||||
{/* content */}
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
{issueDetailStore?.loader && !issue ? (
|
||||
<div className="text-center py-10">Loading...</div>
|
||||
) : (
|
||||
issue && (
|
||||
<>
|
||||
{["side-peek", "modal"].includes(peekMode) ? (
|
||||
<div className="flex flex-col gap-3 px-10 py-6">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
issueReactions={issueReactions}
|
||||
issueUpdate={issueUpdate}
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
|
||||
<div className="border-t border-custom-border-400" />
|
||||
<PeekOverviewProperties issue={issue} issueUpdate={issueUpdate} user={user} />
|
||||
|
||||
<IssueComment
|
||||
workspaceSlug={workspaceSlug}
|
||||
@ -253,23 +271,46 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
|
||||
<PeekOverviewProperties
|
||||
issue={issue}
|
||||
issueUpdate={issueUpdate}
|
||||
states={states}
|
||||
members={members}
|
||||
priorities={priorities}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex">
|
||||
<div className="w-full h-full space-y-6 p-4 py-5">
|
||||
<PeekOverviewIssueDetails
|
||||
workspaceSlug={workspaceSlug}
|
||||
issue={issue}
|
||||
issueReactions={issueReactions}
|
||||
issueUpdate={issueUpdate}
|
||||
user={user}
|
||||
issueReactionCreate={issueReactionCreate}
|
||||
issueReactionRemove={issueReactionRemove}
|
||||
/>
|
||||
|
||||
<div className="border-t border-custom-border-400" />
|
||||
|
||||
<IssueComment
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
issueComments={issueComments}
|
||||
issueCommentCreate={issueCommentCreate}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-shrink-0 !w-[400px] h-full border-l border-custom-border-200 p-4 py-5">
|
||||
<PeekOverviewProperties issue={issue} issueUpdate={issueUpdate} user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -16,13 +16,20 @@ type Props = {
|
||||
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabled = false }) => {
|
||||
const { estimatePoints } = useEstimateOption();
|
||||
|
||||
const currentEstimate = estimatePoints?.find((e) => e.key === value)?.value;
|
||||
return (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1.5 !text-sm bg-custom-background-80 rounded px-2.5 py-0.5">
|
||||
<Triangle className={`h-4 w-4 ${value !== null ? "text-custom-text-100" : "text-custom-text-200"}`} />
|
||||
{estimatePoints?.find((e) => e.key === value)?.value ?? "No estimate"}
|
||||
<div className="flex items-center gap-1.5 text-xs bg-custom-background-80 rounded px-2.5 py-0.5">
|
||||
{currentEstimate ? (
|
||||
<>
|
||||
<Triangle className={`h-3 w-3 ${value !== null ? "text-custom-text-100" : "text-custom-text-200"}`} />
|
||||
{currentEstimate}
|
||||
</>
|
||||
) : (
|
||||
"No Estimate"
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
@ -31,7 +38,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
<span>
|
||||
<Triangle className="h-4 w-4" />
|
||||
<Triangle className="h-3.5 w-3" />
|
||||
</span>
|
||||
None
|
||||
</>
|
||||
@ -41,7 +48,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, disabl
|
||||
<CustomSelect.Option key={point.key} value={point.key}>
|
||||
<>
|
||||
<span>
|
||||
<Triangle className="h-4 w-4" />
|
||||
<Triangle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
{point.value}
|
||||
</>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { Controller, UseFormWatch, useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
// headless ui
|
||||
import { Listbox, Popover, Transition } from "@headlessui/react";
|
||||
@ -12,7 +12,7 @@ import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
// icons
|
||||
import { Component, Plus, Tag, X } from "lucide-react";
|
||||
import { Component, Plus, X } from "lucide-react";
|
||||
// types
|
||||
import { IIssue, IIssueLabels } from "types";
|
||||
// fetch-keys
|
||||
@ -20,8 +20,7 @@ import { PROJECT_ISSUE_LABELS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
issueDetails: IIssue | undefined;
|
||||
issueControl: any;
|
||||
watchIssue: UseFormWatch<IIssue>;
|
||||
labelList: string[];
|
||||
submitChanges: (formData: any) => void;
|
||||
isNotAllowed: boolean;
|
||||
uneditable: boolean;
|
||||
@ -36,8 +35,7 @@ const issueLabelService = new IssueLabelService();
|
||||
|
||||
export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
issueDetails,
|
||||
issueControl,
|
||||
watchIssue,
|
||||
labelList,
|
||||
submitChanges,
|
||||
isNotAllowed,
|
||||
uneditable,
|
||||
@ -91,167 +89,152 @@ export const SidebarLabelSelect: React.FC<Props> = ({
|
||||
}, [createLabelForm, reset, setFocus]);
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 py-3 ${uneditable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex basis-1/2 items-center gap-x-2 text-sm text-custom-text-200">
|
||||
<Tag className="h-4 w-4" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div className="basis-1/2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{watchIssue("labels")?.map((labelId) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{labelList?.map((labelId) => {
|
||||
const label = issueLabels?.find((l) => l.id === labelId);
|
||||
|
||||
if (label)
|
||||
return (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs hover:border-red-500/20 hover:bg-red-500/20"
|
||||
onClick={() => {
|
||||
const updatedLabels = watchIssue("labels")?.filter((l) => l !== labelId);
|
||||
submitChanges({
|
||||
labels: updatedLabels,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
<X className="h-2 w-2 group-hover:text-red-500" />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<Controller
|
||||
control={issueControl}
|
||||
name="labels"
|
||||
render={({ field: { value } }) => (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={value}
|
||||
onChange={(val: any) => submitChanges({ labels: val })}
|
||||
className="flex-shrink-0"
|
||||
multiple
|
||||
disabled={isNotAllowed || uneditable}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed || uneditable
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
|
||||
>
|
||||
Select Label
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{issueLabels ? (
|
||||
issueLabels.length > 0 ? (
|
||||
issueLabels.map((label: IIssueLabels) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={label.id}
|
||||
className={({ active, selected }) =>
|
||||
`${active || selected ? "bg-custom-background-90" : ""} ${
|
||||
selected ? "" : "text-custom-text-200"
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</Listbox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-100 bg-custom-background-90">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-custom-text-100">
|
||||
<Component className="h-3 w-3" />
|
||||
{label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Listbox.Option
|
||||
key={child.id}
|
||||
className={({ active, selected }) =>
|
||||
`${active || selected ? "bg-custom-background-100" : ""} ${
|
||||
selected ? "" : "text-custom-text-200"
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
|
||||
}
|
||||
value={child.id}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: child?.color ?? "black",
|
||||
}}
|
||||
/>
|
||||
{child.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center">No labels found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
if (label)
|
||||
return (
|
||||
<span
|
||||
key={label.id}
|
||||
className="group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs hover:border-red-500/20 hover:bg-red-500/20"
|
||||
onClick={() => {
|
||||
const updatedLabels = labelList?.filter((l) => l !== labelId);
|
||||
submitChanges({
|
||||
labels: updatedLabels,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label?.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
<X className="h-2 w-2 group-hover:text-red-500" />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issueDetails?.labels ?? []}
|
||||
onChange={(val: any) => submitChanges({ labels: val })}
|
||||
className="flex-shrink-0"
|
||||
multiple
|
||||
disabled={isNotAllowed || uneditable}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
disabled={uneditable}
|
||||
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
<>
|
||||
<X className="h-3 w-3" /> Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-3 w-3" /> New
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
Select Label
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-28 w-40 overflow-auto rounded-md bg-custom-background-80 py-1 text-xs shadow-lg border border-custom-border-100 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{issueLabels ? (
|
||||
issueLabels.length > 0 ? (
|
||||
issueLabels.map((label: IIssueLabels) => {
|
||||
const children = issueLabels?.filter((l) => l.parent === label.id);
|
||||
|
||||
if (children.length === 0) {
|
||||
if (!label.parent)
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={label.id}
|
||||
className={({ active, selected }) =>
|
||||
`${active || selected ? "bg-custom-background-90" : ""} ${
|
||||
selected ? "" : "text-custom-text-200"
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color && label.color !== "" ? label.color : "#000",
|
||||
}}
|
||||
/>
|
||||
{label.name}
|
||||
</Listbox.Option>
|
||||
);
|
||||
} else
|
||||
return (
|
||||
<div className="border-y border-custom-border-100 bg-custom-background-90">
|
||||
<div className="flex select-none items-center gap-2 truncate p-2 font-medium text-custom-text-100">
|
||||
<Component className="h-3 w-3" />
|
||||
{label.name}
|
||||
</div>
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<Listbox.Option
|
||||
key={child.id}
|
||||
className={({ active, selected }) =>
|
||||
`${active || selected ? "bg-custom-background-100" : ""} ${
|
||||
selected ? "" : "text-custom-text-200"
|
||||
} flex cursor-pointer select-none items-center gap-2 truncate p-2`
|
||||
}
|
||||
value={child.id}
|
||||
>
|
||||
<span
|
||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
backgroundColor: child?.color ?? "black",
|
||||
}}
|
||||
/>
|
||||
{child.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center">No labels found</div>
|
||||
)
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className={`flex ${
|
||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs hover:text-custom-text-200 text-custom-text-300`}
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
disabled={uneditable}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
<>
|
||||
<X className="h-3 w-3" /> Cancel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-3 w-3" /> New
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{createLabelForm && (
|
||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
||||
<div>
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
// icons
|
||||
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Trash2, Triangle, User2 } from "lucide-react";
|
||||
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
|
||||
import { ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
@ -333,7 +333,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
@ -354,7 +354,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="assignees"
|
||||
@ -375,7 +375,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="priority"
|
||||
@ -583,7 +583,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Cycle</p>
|
||||
</div>
|
||||
<div className="space-y-1 sm:w-1/2">
|
||||
<div className="space-y-1">
|
||||
<SidebarCycleSelect
|
||||
issueDetail={issueDetail}
|
||||
handleCycleChange={handleCycleChange}
|
||||
@ -598,7 +598,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Module</p>
|
||||
</div>
|
||||
<div className="space-y-1 sm:w-1/2">
|
||||
<div className="space-y-1">
|
||||
<SidebarModuleSelect
|
||||
issueDetail={issueDetail}
|
||||
handleModuleChange={handleModuleChange}
|
||||
@ -611,14 +611,21 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<SidebarLabelSelect
|
||||
issueDetails={issueDetail}
|
||||
issueControl={control}
|
||||
watchIssue={watchIssue}
|
||||
submitChanges={submitChanges}
|
||||
isNotAllowed={isNotAllowed}
|
||||
uneditable={uneditable ?? false}
|
||||
/>
|
||||
<div className="flex flex-wrap items-start py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div className="space-y-1 sm:w-1/2">
|
||||
<SidebarLabelSelect
|
||||
issueDetails={issueDetail}
|
||||
labelList={issueDetail?.labels ?? []}
|
||||
submitChanges={submitChanges}
|
||||
isNotAllowed={isNotAllowed}
|
||||
uneditable={uneditable ?? false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<div className={`min-h-[116px] py-1 text-xs ${uneditable ? "opacity-60" : ""}`}>
|
||||
|
@ -8,14 +8,12 @@ import { IssueService } from "services/issue";
|
||||
import { TrackEventService } from "services/track_event.service";
|
||||
// components
|
||||
import { ViewDueDateSelect, ViewStartDateSelect } from "components/issues";
|
||||
import { MembersSelect, PrioritySelect } from "components/project";
|
||||
import { StateSelect } from "components/states";
|
||||
// helpers
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
import { PrioritySelect } from "components/project";
|
||||
// types
|
||||
import { IUser, IIssue, IState } from "types";
|
||||
// fetch-keys
|
||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||
import { IssuePropertyAssignee, IssuePropertyState } from "../issue-layouts/properties";
|
||||
|
||||
export interface IIssueProperty {
|
||||
workspaceSlug: string;
|
||||
@ -32,7 +30,7 @@ const trackEventService = new TrackEventService();
|
||||
export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
|
||||
const { workspaceSlug, parentIssue, issue, user, editable } = props;
|
||||
|
||||
const { project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||
const { issueFilter: issueFilterStore } = useMobxStore();
|
||||
|
||||
const displayProperties = issueFilterStore.userDisplayProperties ?? {};
|
||||
|
||||
@ -117,8 +115,6 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const statesList = getStatesList(projectStore.states?.[issue.project]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center gap-1">
|
||||
{displayProperties.priority && (
|
||||
@ -134,12 +130,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
|
||||
|
||||
{displayProperties.state && (
|
||||
<div className="flex-shrink-0">
|
||||
<StateSelect
|
||||
value={issue.state_detail}
|
||||
states={statesList}
|
||||
<IssuePropertyState
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.state_detail || null}
|
||||
onChange={(data) => handleStateChange(data)}
|
||||
hideDropdownArrow
|
||||
disabled={!editable}
|
||||
disabled={false}
|
||||
hideDropdownArrow={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -168,13 +164,12 @@ export const IssueProperty: React.FC<IIssueProperty> = observer((props) => {
|
||||
|
||||
{displayProperties.assignee && (
|
||||
<div className="flex-shrink-0">
|
||||
<MembersSelect
|
||||
value={issue.assignees}
|
||||
<IssuePropertyAssignee
|
||||
projectId={issue?.project_detail?.id || null}
|
||||
value={issue?.assignees || null}
|
||||
hideDropdownArrow={true}
|
||||
onChange={(val) => handleAssigneeChange(val)}
|
||||
members={projectStore.members ? (projectStore.members[issue.project] ?? []).map((m) => m.member) : []}
|
||||
hideDropdownArrow
|
||||
disabled={!editable}
|
||||
multiple
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
// services
|
||||
import { IssueService, IssueReactionService, IssueCommentService } from "services/issue";
|
||||
import { NotificationService } from "services/notification.service";
|
||||
// types
|
||||
import { RootStore } from "../root";
|
||||
import { IIssue } from "types";
|
||||
@ -28,6 +29,9 @@ export interface IIssueDetailStore {
|
||||
[comment_id: string]: any;
|
||||
};
|
||||
};
|
||||
issue_subscription: {
|
||||
[issueId: string]: any;
|
||||
};
|
||||
|
||||
setPeekId: (issueId: string | null) => void;
|
||||
|
||||
@ -36,6 +40,7 @@ export interface IIssueDetailStore {
|
||||
getIssueReactions: any | null;
|
||||
getIssueComments: any | null;
|
||||
getIssueCommentReactions: any | null;
|
||||
getIssueSubscription: any | null;
|
||||
|
||||
// fetch issue details
|
||||
fetchIssueDetails: (workspaceSlug: string, projectId: string, issueId: string) => Promise<IIssue>;
|
||||
@ -84,6 +89,10 @@ export interface IIssueDetailStore {
|
||||
commentId: string,
|
||||
reaction: string
|
||||
) => Promise<void>;
|
||||
|
||||
fetchIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
createIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeIssueSubscription: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export class IssueDetailStore implements IIssueDetailStore {
|
||||
@ -103,6 +112,9 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
issue_comment_reactions: {
|
||||
[issueId: string]: any;
|
||||
} = {};
|
||||
issue_subscription: {
|
||||
[issueId: string]: any;
|
||||
} = {};
|
||||
|
||||
// root store
|
||||
rootStore;
|
||||
@ -110,6 +122,7 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
issueService;
|
||||
issueReactionService;
|
||||
issueCommentService;
|
||||
notificationService;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
@ -122,11 +135,13 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
issue_reactions: observable.ref,
|
||||
issue_comments: observable.ref,
|
||||
issue_comment_reactions: observable.ref,
|
||||
issue_subscription: observable.ref,
|
||||
|
||||
getIssue: computed,
|
||||
getIssueReactions: computed,
|
||||
getIssueComments: computed,
|
||||
getIssueCommentReactions: computed,
|
||||
getIssueSubscription: computed,
|
||||
|
||||
setPeekId: action,
|
||||
|
||||
@ -150,12 +165,17 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
fetchIssueCommentReactions: action,
|
||||
creationIssueCommentReaction: action,
|
||||
removeIssueCommentReaction: action,
|
||||
|
||||
fetchIssueSubscription: action,
|
||||
createIssueSubscription: action,
|
||||
removeIssueSubscription: action,
|
||||
});
|
||||
|
||||
this.rootStore = _rootStore;
|
||||
this.issueService = new IssueService();
|
||||
this.issueReactionService = new IssueReactionService();
|
||||
this.issueCommentService = new IssueCommentService();
|
||||
this.notificationService = new NotificationService();
|
||||
}
|
||||
|
||||
get getIssue() {
|
||||
@ -182,6 +202,12 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
return _commentReactions || null;
|
||||
}
|
||||
|
||||
get getIssueSubscription() {
|
||||
if (!this.peekId) return null;
|
||||
const _commentSubscription = this.issue_subscription[this.peekId];
|
||||
return _commentSubscription || null;
|
||||
}
|
||||
|
||||
setPeekId = (issueId: string | null) => (this.peekId = issueId);
|
||||
|
||||
fetchIssueDetails = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
@ -662,4 +688,61 @@ export class IssueDetailStore implements IIssueDetailStore {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// subscription
|
||||
fetchIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const _subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId
|
||||
);
|
||||
|
||||
const _issue_subscription = {
|
||||
...this.issue_subscription,
|
||||
[issueId]: _subscription,
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_subscription = _issue_subscription;
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("error fetching the issue subscription", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
createIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
|
||||
|
||||
const _issue_subscription = {
|
||||
...this.issue_subscription,
|
||||
[issueId]: { subscribed: true },
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_subscription = _issue_subscription;
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("error creating the issue subscription", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
removeIssueSubscription = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const _issue_subscription = {
|
||||
...this.issue_subscription,
|
||||
[issueId]: { subscribed: false },
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
this.issue_subscription = _issue_subscription;
|
||||
});
|
||||
|
||||
await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
|
||||
} catch (error) {
|
||||
console.warn("error removing the issue subscription", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user