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