diff --git a/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx new file mode 100644 index 000000000..3efebfd8e --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx @@ -0,0 +1,85 @@ +import React from "react"; +// react beautiful dnd +import { DragDropContext } from "@hello-pangea/dnd"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { KanBanSwimLanes } from "./swimlanes"; +import { KanBan } from "./default"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export interface IProfileIssuesKanBanLayout {} + +export const ProfileIssuesKanBanLayout: React.FC = observer(() => { + const { + profileIssues: profileIssuesStore, + profileIssueFilters: profileIssueFiltersStore, + issueKanBanView: issueKanBanViewStore, + }: RootStore = useMobxStore(); + + const issues = profileIssuesStore?.getIssues; + + const sub_group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by || null; + + const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; + + const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; + + const currentKanBanView: "swimlanes" | "default" = profileIssueFiltersStore?.userDisplayFilters?.sub_group_by + ? "swimlanes" + : "default"; + + const onDragEnd = (result: any) => { + if (!result) return; + + if ( + result.destination && + result.source && + result.destination.droppableId === result.source.droppableId && + result.destination.index === result.source.index + ) + return; + + currentKanBanView === "default" + ? issueKanBanViewStore?.handleDragDrop(result.source, result.destination) + : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); + }; + + const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { + profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); + }; + + const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { + issueKanBanViewStore.handleKanBanToggle(toggle, value); + }; + + return ( +
+ + {currentKanBanView === "default" ? ( + + ) : ( + + )} + +
+ ); +}); diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index cefde1798..979ead23b 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -8,7 +8,7 @@ import { IssuePropertyPriority } from "../properties/priority"; import { IssuePropertyLabels } from "../properties/labels"; import { IssuePropertyAssignee } from "../properties/assignee"; import { IssuePropertyEstimates } from "../properties/estimates"; -import { IssuePropertyStartDate } from "../properties/date"; +import { IssuePropertyDate } from "../properties/date"; import { Tooltip } from "@plane/ui"; export interface IKanBanProperties { @@ -129,7 +129,7 @@ export const KanBanProperties: React.FC = observer( {/* start date */} {display_properties && display_properties?.start_date && ( - handleStartDate(date)} disabled={false} @@ -138,7 +138,7 @@ export const KanBanProperties: React.FC = observer( {/* target/due date */} {display_properties && display_properties?.due_date && ( - handleTargetDate(date)} disabled={false} diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/root.tsx index 6e93e0b23..d3aa148f8 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/root.tsx @@ -1,7 +1,5 @@ -import React from "react"; -// react beautiful dnd +import { FC } from "react"; import { DragDropContext } from "@hello-pangea/dnd"; -// mobx import { observer } from "mobx-react-lite"; // components import { KanBanSwimLanes } from "./swimlanes"; @@ -9,11 +7,14 @@ import { KanBan } from "./default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IKanBanLayout {} -export const KanBanLayout: React.FC = observer(() => { +export const KanBanLayout: FC = observer(() => { const { + project: projectStore, issue: issueStore, issueFilter: issueFilterStore, issueKanBanView: issueKanBanViewStore, @@ -55,6 +56,14 @@ export const KanBanLayout: React.FC = observer(() => { issueKanBanViewStore.handleKanBanToggle(toggle, value); }; + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = projectStore?.projectLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.projectStates || null; + const estimates = null; + return (
@@ -67,6 +76,13 @@ export const KanBanLayout: React.FC = observer(() => { display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} + // states={states} + // stateGroups={stateGroups} + // priorities={priorities} + // labels={labels} + // members={members} + // projects={projects} + // estimates={estimates} /> ) : ( { display_properties={display_properties} kanBanToggle={issueKanBanViewStore?.kanBanToggle} handleKanBanToggle={handleKanBanToggle} + // states={states} + // stateGroups={stateGroups} + // priorities={priorities} + // labels={labels} + // members={members} + // projects={projects} + // estimates={estimates} /> )} diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index a54e47a01..8c5cbb473 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,41 +1,52 @@ +import { FC } from "react"; // components import { KanBanProperties } from "./properties"; +// ui +import { Tooltip } from "@plane/ui"; interface IssueBlockProps { columnId: string; issues: any; handleIssues?: (group_by: string | null, issue: any) => void; display_properties: any; + states: any; + labels: any; + members: any; + priorities: any; } -export const IssueBlock = ({ columnId, issues, handleIssues, display_properties }: IssueBlockProps) => ( - <> - {issues && issues.length > 0 ? ( - <> - {issues.map((issue: any, index: any) => ( +export const IssueBlock: FC = (props) => { + const { columnId, issues, handleIssues, display_properties, states, labels, members, priorities } = props; + + return ( + <> + {issues && + issues?.length > 0 && + issues.map((issue: any, index: any) => (
{display_properties && display_properties?.key && (
ONE-{issue.sequence_id}
)} -
{issue.name}
+ +
{issue.name}
+
))} - - ) : ( -
- No issues are available -
- )} - -); + + ); +}; diff --git a/web/components/issues/issue-layouts/list/cycle-root.tsx b/web/components/issues/issue-layouts/list/cycle-root.tsx index 827558936..389656950 100644 --- a/web/components/issues/issue-layouts/list/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/cycle-root.tsx @@ -1,16 +1,21 @@ import React from "react"; -// mobx import { observer } from "mobx-react-lite"; // components import { List } from "./default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { - const { issueFilter: issueFilterStore, cycleIssue: cycleIssueStore }: RootStore = useMobxStore(); + const { + project: projectStore, + issueFilter: issueFilterStore, + cycleIssue: cycleIssueStore, + }: RootStore = useMobxStore(); const issues = cycleIssueStore?.getIssues; @@ -22,9 +27,29 @@ export const CycleListLayout: React.FC = observer(() => { cycleIssueStore.updateIssueStructure(group_by, null, issue); }; + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = projectStore?.projectLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.projectStates || null; + const estimates = null; + return (
- +
); }); diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 16c6a35d8..b3ec8a009 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,130 +1,256 @@ import React from "react"; +import { observer } from "mobx-react-lite"; // components -import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; +import { ListGroupByHeaderRoot } from "./headers/group-by-root"; import { IssueBlock } from "./block"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; -// mobx -import { observer } from "mobx-react-lite"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; +import { getValueFromObject } from "constants/issue"; -export interface IGroupByKanBan { +export interface IGroupByList { issues: any; group_by: string | null; list: any; listKey: string; handleIssues?: (group_by: string | null, issue: any) => void; display_properties: any; + is_list?: boolean; + states: any; + labels: any; + members: any; + projects: any; + stateGroups: any; + priorities: any; + estimates: any; } -const GroupByKanBan: React.FC = observer( - ({ issues, group_by, list, listKey, handleIssues, display_properties }) => ( +const GroupByList: React.FC = observer((props) => { + const { + issues, + group_by, + list, + listKey, + handleIssues, + display_properties, + is_list = false, + states, + labels, + members, + projects, + stateGroups, + priorities, + estimates, + } = props; + + return (
{list && list.length > 0 && list.map((_list: any) => ( -
-
- +
+
{issues && ( )}
))}
- ) -); + ); +}); -export interface IKanBan { +export interface IList { issues: any; group_by: string | null; handleDragDrop?: (result: any) => void | undefined; handleIssues?: (group_by: string | null, issue: any) => void; display_properties: any; + states: any; + labels: any; + members: any; + projects: any; + stateGroups: any; + priorities: any; + estimates: any; } -export const List: React.FC = observer(({ issues, group_by, handleIssues, display_properties }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const List: React.FC = observer((props) => { + const { + issues, + group_by, + handleIssues, + display_properties, + states, + labels, + members, + projects, + stateGroups, + priorities, + estimates, + } = props; return (
- {group_by && group_by === "state" && ( - )} - {group_by && group_by === "state_detail.group" && ( - - )} - - {group_by && group_by === "priority" && ( - - )} - - {group_by && group_by === "labels" && ( - )} - {group_by && group_by === "assignees" && ( - )} - {group_by && group_by === "created_by" && ( - + )} + + {group_by && group_by === "priority" && priorities && ( + + )} + + {group_by && group_by === "labels" && labels && ( + + )} + + {group_by && group_by === "assignees" && members && ( + + )} + + {group_by && group_by === "created_by" && members && ( + )}
diff --git a/web/components/issues/issue-layouts/list/headers/assignee.tsx b/web/components/issues/issue-layouts/list/headers/assignee.tsx index 0c46dc029..5cfdc5b06 100644 --- a/web/components/issues/issue-layouts/list/headers/assignee.tsx +++ b/web/components/issues/issue-layouts/list/headers/assignee.tsx @@ -1,23 +1,21 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { HeaderGroupByCard } from "./group-by-card"; import { Avatar } from "components/ui"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface IAssigneesHeader { column_id: string; + column_value: any; issues_count: number; } export const Icon = ({ user }: any) => ; -export const AssigneesHeader: React.FC = observer(({ column_id, issues_count }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const AssigneesHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; - const assignee = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null; + const assignee = column_value ?? null; return ( <> diff --git a/web/components/issues/issue-layouts/list/headers/created_by.tsx b/web/components/issues/issue-layouts/list/headers/created-by.tsx similarity index 56% rename from web/components/issues/issue-layouts/list/headers/created_by.tsx rename to web/components/issues/issue-layouts/list/headers/created-by.tsx index 92b074936..308841c38 100644 --- a/web/components/issues/issue-layouts/list/headers/created_by.tsx +++ b/web/components/issues/issue-layouts/list/headers/created-by.tsx @@ -1,21 +1,19 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { HeaderGroupByCard } from "./group-by-card"; import { Icon } from "./assignee"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface ICreatedByHeader { column_id: string; + column_value: any; issues_count: number; } -export const CreatedByHeader: React.FC = observer(({ column_id, issues_count }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const CreatedByHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; - const createdBy = (column_id && projectStore?.getProjectMemberByUserId(column_id)) ?? null; + const createdBy = column_value ?? null; return ( <> diff --git a/web/components/issues/issue-layouts/list/headers/empty-group.tsx b/web/components/issues/issue-layouts/list/headers/empty-group.tsx new file mode 100644 index 000000000..2449a3fd8 --- /dev/null +++ b/web/components/issues/issue-layouts/list/headers/empty-group.tsx @@ -0,0 +1,15 @@ +import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; + +export interface IEmptyHeader { + column_id: string; + column_value: any; + issues_count: number; +} + +export const EmptyHeader: React.FC = observer((props) => { + const { column_id, column_value, issues_count } = props; + + return ; +}); diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 337991838..77ef79769 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,6 +1,6 @@ import React from "react"; // lucide icons -import { Circle } from "lucide-react"; +import { CircleDashed } from "lucide-react"; // mobx import { observer } from "mobx-react-lite"; @@ -20,7 +20,7 @@ export const HeaderGroupByCard = observer(({ icon, title, count }: IHeaderGroupB }`} >
- {icon ? icon : } + {icon ? icon : }
diff --git a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx b/web/components/issues/issue-layouts/list/headers/group-by-root.tsx index f4a4f3820..5a724a8d4 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-root.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-root.tsx @@ -1,30 +1,52 @@ // components +import { EmptyHeader } from "./empty-group"; +import { ProjectHeader } from "./project"; import { StateHeader } from "./state"; import { StateGroupHeader } from "./state-group"; import { AssigneesHeader } from "./assignee"; import { PriorityHeader } from "./priority"; import { LabelHeader } from "./label"; -import { CreatedByHeader } from "./created_by"; +import { CreatedByHeader } from "./created-by"; // mobx import { observer } from "mobx-react-lite"; -export interface IKanBanGroupByHeaderRoot { +export interface IListGroupByHeaderRoot { column_id: string; + column_value: any; group_by: string | null; issues_count: number; } -export const KanBanGroupByHeaderRoot: React.FC = observer( - ({ column_id, group_by, issues_count }) => ( +export const ListGroupByHeaderRoot: React.FC = observer((props) => { + const { column_id, column_value, group_by, issues_count } = props; + + return ( <> - {group_by && group_by === "state" && } - {group_by && group_by === "state_detail.group" && ( - + {!group_by && group_by === null && ( + + )} + {group_by && group_by === "project" && ( + + )} + + {group_by && group_by === "state" && ( + + )} + {group_by && group_by === "state_detail.group" && ( + + )} + {group_by && group_by === "priority" && ( + + )} + {group_by && group_by === "labels" && ( + + )} + {group_by && group_by === "assignees" && ( + + )} + {group_by && group_by === "created_by" && ( + )} - {group_by && group_by === "priority" && } - {group_by && group_by === "labels" && } - {group_by && group_by === "assignees" && } - {group_by && group_by === "created_by" && } - ) -); + ); +}); diff --git a/web/components/issues/issue-layouts/list/headers/label.tsx b/web/components/issues/issue-layouts/list/headers/label.tsx index d7a6c0253..f200bab57 100644 --- a/web/components/issues/issue-layouts/list/headers/label.tsx +++ b/web/components/issues/issue-layouts/list/headers/label.tsx @@ -1,13 +1,11 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { HeaderGroupByCard } from "./group-by-card"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface ILabelHeader { column_id: string; + column_value: any; issues_count: number; } @@ -15,10 +13,20 @@ const Icon = ({ color }: any) => (
); -export const LabelHeader: React.FC = observer(({ column_id, issues_count }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const LabelHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; - const label = (column_id && projectStore?.getProjectLabelById(column_id)) ?? null; + const label = column_value ?? null; - return <>{label && } title={label?.name || ""} count={issues_count} />}; + return ( + <> + {column_value && ( + } + title={column_value?.name || ""} + count={issues_count} + /> + )} + + ); }); diff --git a/web/components/issues/issue-layouts/list/headers/priority.tsx b/web/components/issues/issue-layouts/list/headers/priority.tsx index 6bc29bb48..4a7576882 100644 --- a/web/components/issues/issue-layouts/list/headers/priority.tsx +++ b/web/components/issues/issue-layouts/list/headers/priority.tsx @@ -1,14 +1,13 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // lucide icons import { AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; // components import { HeaderGroupByCard } from "./group-by-card"; -// constants -import { issuePriorityByKey } from "constants/issue"; export interface IPriorityHeader { column_id: string; + column_value: any; issues_count: number; } @@ -38,8 +37,10 @@ const Icon = ({ priority }: any) => (
); -export const PriorityHeader: React.FC = observer(({ column_id, issues_count }) => { - const priority = column_id && issuePriorityByKey(column_id); +export const PriorityHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; + + const priority = column_value ?? null; return ( <> diff --git a/web/components/issues/issue-layouts/list/headers/project.tsx b/web/components/issues/issue-layouts/list/headers/project.tsx new file mode 100644 index 000000000..cb83068ab --- /dev/null +++ b/web/components/issues/issue-layouts/list/headers/project.tsx @@ -0,0 +1,28 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { HeaderGroupByCard } from "./group-by-card"; +// emoji helper +import { renderEmoji } from "helpers/emoji.helper"; + +export interface IProjectHeader { + column_id: string; + column_value: any; + issues_count: number; +} + +const Icon = ({ emoji }: any) =>
{renderEmoji(emoji)}
; + +export const ProjectHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; + + const project = column_value ?? null; + + return ( + <> + {project && ( + } title={project?.name || ""} count={issues_count} /> + )} + + ); +}); diff --git a/web/components/issues/issue-layouts/list/headers/state-group.tsx b/web/components/issues/issue-layouts/list/headers/state-group.tsx index 203fdd2ac..e97def5d8 100644 --- a/web/components/issues/issue-layouts/list/headers/state-group.tsx +++ b/web/components/issues/issue-layouts/list/headers/state-group.tsx @@ -1,13 +1,13 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { HeaderGroupByCard } from "./group-by-card"; +// ui import { StateGroupIcon } from "@plane/ui"; -// constants -import { issueStateGroupByKey } from "constants/issue"; export interface IStateGroupHeader { column_id: string; + column_value: any; issues_count: number; } @@ -17,8 +17,10 @@ export const Icon = ({ stateGroup, color }: { stateGroup: any; color?: any }) =>
); -export const StateGroupHeader: React.FC = observer(({ column_id, issues_count }) => { - const stateGroup = column_id && issueStateGroupByKey(column_id); +export const StateGroupHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; + + const stateGroup = column_value ?? null; return ( <> diff --git a/web/components/issues/issue-layouts/list/headers/state.tsx b/web/components/issues/issue-layouts/list/headers/state.tsx index 4210cffe6..419bfb367 100644 --- a/web/components/issues/issue-layouts/list/headers/state.tsx +++ b/web/components/issues/issue-layouts/list/headers/state.tsx @@ -1,21 +1,19 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { HeaderGroupByCard } from "./group-by-card"; import { Icon } from "./state-group"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; export interface IStateHeader { column_id: string; + column_value: any; issues_count: number; } -export const StateHeader: React.FC = observer(({ column_id, issues_count }) => { - const { project: projectStore }: RootStore = useMobxStore(); +export const StateHeader: FC = observer((props) => { + const { column_id, column_value, issues_count } = props; - const state = (column_id && projectStore?.getProjectStateById(column_id)) ?? null; + const state = column_value ?? null; return ( <> diff --git a/web/components/issues/issue-layouts/list/module-root.tsx b/web/components/issues/issue-layouts/list/module-root.tsx index 66f8771f7..863467b56 100644 --- a/web/components/issues/issue-layouts/list/module-root.tsx +++ b/web/components/issues/issue-layouts/list/module-root.tsx @@ -1,16 +1,21 @@ import React from "react"; -// mobx import { observer } from "mobx-react-lite"; // components import { List } from "./default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { - const { issueFilter: issueFilterStore, moduleIssue: moduleIssueStore }: RootStore = useMobxStore(); + const { + project: projectStore, + issueFilter: issueFilterStore, + moduleIssue: moduleIssueStore, + }: RootStore = useMobxStore(); const issues = moduleIssueStore?.getIssues; @@ -22,9 +27,29 @@ export const ModuleListLayout: React.FC = observer(() => { moduleIssueStore.updateIssueStructure(group_by, null, issue); }; + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = projectStore?.projectLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.projectStates || null; + const estimates = null; + return (
- +
); }); diff --git a/web/components/issues/issue-layouts/list/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/profile-issues-root.tsx new file mode 100644 index 000000000..5ee9958aa --- /dev/null +++ b/web/components/issues/issue-layouts/list/profile-issues-root.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { List } from "./default"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; + +export interface IProfileIssuesListLayout {} + +export const ProfileIssuesListLayout: FC = observer(() => { + const { + workspace: workspaceStore, + project: projectStore, + profileIssueFilters: profileIssueFiltersStore, + profileIssues: profileIssuesIssueStore, + }: RootStore = useMobxStore(); + + const issues = profileIssuesIssueStore?.getIssues; + + const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; + + const display_properties = profileIssueFiltersStore?.userDisplayProperties || null; + + const updateIssue = (group_by: string | null, issue: any) => { + profileIssuesIssueStore.updateIssueStructure(group_by, null, issue); + }; + + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = workspaceStore.workspaceLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.workspaceProjects || null; + const estimates = null; + + return ( +
+ +
+ ); +}); diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 6de888de1..953b6ba4d 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -1,6 +1,5 @@ -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; -// lucide icons import { Layers, Link, Paperclip } from "lucide-react"; // components import { IssuePropertyState } from "../properties/state"; @@ -8,7 +7,8 @@ import { IssuePropertyPriority } from "../properties/priority"; import { IssuePropertyLabels } from "../properties/labels"; import { IssuePropertyAssignee } from "../properties/assignee"; import { IssuePropertyEstimates } from "../properties/estimates"; -import { IssuePropertyStartDate } from "../properties/date"; +import { IssuePropertyDate } from "../properties/date"; +// ui import { Tooltip } from "@plane/ui"; export interface IKanBanProperties { @@ -16,151 +16,159 @@ export interface IKanBanProperties { issue: any; handleIssues?: (group_by: string | null, issue: any) => void; display_properties: any; + states: any; + labels: any; + members: any; + priorities: any; } -export const KanBanProperties: React.FC = observer( - ({ columnId: group_id, issue, handleIssues, display_properties }) => { - const handleState = (id: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: id }); - }; +export const KanBanProperties: FC = observer((props) => { + const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, priorities } = props; - const handlePriority = (id: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: id }); - }; + const handleState = (id: string) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: id }); + }; - const handleLabel = (ids: string[]) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); - }; + const handlePriority = (id: string) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: id }); + }; - const handleAssignee = (ids: string[]) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); - }; + const handleLabel = (ids: string[]) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); + }; - const handleStartDate = (date: string) => { - if (handleIssues) - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); - }; + const handleAssignee = (ids: string[]) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); + }; - const handleTargetDate = (date: string) => { - if (handleIssues) - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); - }; + const handleStartDate = (date: string) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); + }; - const handleEstimate = (id: string) => { - if (handleIssues) - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: id }); - }; + const handleTargetDate = (date: string) => { + if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); + }; - return ( -
- {/* basic properties */} - {/* state */} - {display_properties && display_properties?.state && ( - handleState(id)} - disabled={false} - /> - )} + const handleEstimate = (id: string) => { + if (handleIssues) + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: id }); + }; - {/* priority */} - {display_properties && display_properties?.priority && ( - handlePriority(id)} - disabled={false} - /> - )} + return ( +
+ {/* basic properties */} + {/* state */} + {display_properties && display_properties?.state && states && ( + handleState(id)} + disabled={false} + list={states} + /> + )} - {/* label */} - {display_properties && display_properties?.labels && ( - handleLabel(ids)} - disabled={false} - /> - )} + {/* priority */} + {display_properties && display_properties?.priority && priorities && ( + handlePriority(id)} + disabled={false} + list={priorities} + /> + )} - {/* assignee */} - {display_properties && display_properties?.assignee && ( - handleAssignee(ids)} - disabled={false} - /> - )} + {/* label */} + {display_properties && display_properties?.labels && labels && ( + handleLabel(ids)} + disabled={false} + list={labels} + /> + )} - {/* start date */} - {display_properties && display_properties?.start_date && ( - handleStartDate(date)} - disabled={false} - /> - )} + {/* assignee */} + {display_properties && display_properties?.assignee && members && ( + handleAssignee(ids)} + disabled={false} + list={members} + /> + )} - {/* target/due date */} - {display_properties && display_properties?.due_date && ( - handleTargetDate(date)} - disabled={false} - /> - )} + {/* start date */} + {display_properties && display_properties?.start_date && ( + handleStartDate(date)} + disabled={false} + placeHolder={`Start date`} + /> + )} - {/* estimates */} - {display_properties && display_properties?.estimate && ( - handleEstimate(id)} - disabled={false} - workspaceSlug={issue?.workspace_detail?.slug || null} - projectId={issue?.project_detail?.id || null} - /> - )} + {/* target/due date */} + {display_properties && display_properties?.due_date && ( + handleTargetDate(date)} + disabled={false} + placeHolder={`Target date`} + /> + )} - {/* extra render properties */} - {/* sub-issues */} - {display_properties && display_properties?.sub_issue_count && ( - -
-
- -
-
{issue.sub_issues_count}
+ {/* estimates */} + {display_properties && display_properties?.estimate && ( + handleEstimate(id)} + disabled={false} + workspaceSlug={issue?.workspace_detail?.slug || null} + projectId={issue?.project_detail?.id || null} + /> + )} + + {/* extra render properties */} + {/* sub-issues */} + {display_properties && display_properties?.sub_issue_count && ( + +
+
+
- - )} +
{issue.sub_issues_count}
+
+
+ )} - {/* attachments */} - {display_properties && display_properties?.attachment_count && ( - -
-
- -
-
{issue.attachment_count}
+ {/* attachments */} + {display_properties && display_properties?.attachment_count && ( + +
+
+
- - )} +
{issue.attachment_count}
+
+
+ )} - {/* link */} - {display_properties && display_properties?.link && ( - -
-
- -
-
{issue.link_count}
+ {/* link */} + {display_properties && display_properties?.link && ( + +
+
+
- - )} -
- ); - } -); +
{issue.link_count}
+
+
+ )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/list/root.tsx b/web/components/issues/issue-layouts/list/root.tsx index d3b7d0d3a..e78159978 100644 --- a/web/components/issues/issue-layouts/list/root.tsx +++ b/web/components/issues/issue-layouts/list/root.tsx @@ -1,16 +1,16 @@ -import React from "react"; -// mobx +import { FC } from "react"; import { observer } from "mobx-react-lite"; // components import { List } from "./default"; -// store +// hooks import { useMobxStore } from "lib/mobx/store-provider"; +// types import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; -export interface IListLayout {} - -export const ListLayout: React.FC = observer(() => { - const { issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); +export const ListLayout: FC = observer(() => { + const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); const issues = issueStore?.getIssues; @@ -22,9 +22,29 @@ export const ListLayout: React.FC = observer(() => { issueStore.updateIssueStructure(group_by, null, issue); }; + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = projectStore?.projectLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.projectStates || null; + const estimates = null; + return (
- +
); }); diff --git a/web/components/issues/issue-layouts/list/view-root.tsx b/web/components/issues/issue-layouts/list/view-root.tsx index 0e3db56c7..daf499045 100644 --- a/web/components/issues/issue-layouts/list/view-root.tsx +++ b/web/components/issues/issue-layouts/list/view-root.tsx @@ -1,16 +1,17 @@ import React from "react"; -// mobx import { observer } from "mobx-react-lite"; // components import { List } from "./default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; +// constants +import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IViewListLayout {} export const ViewListLayout: React.FC = observer(() => { - const { issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); + const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); const issues = issueStore?.getIssues; @@ -22,9 +23,29 @@ export const ViewListLayout: React.FC = observer(() => { issueStore.updateIssueStructure(group_by, null, issue); }; + const states = projectStore?.projectStates || null; + const priorities = ISSUE_PRIORITIES || null; + const labels = projectStore?.projectLabels || null; + const members = projectStore?.projectMembers || null; + const stateGroups = ISSUE_STATE_GROUPS || null; + const projects = projectStore?.projectStates || null; + const estimates = null; + return (
- +
); }); diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx index 6ce84de8b..a235ec5a2 100644 --- a/web/components/issues/issue-layouts/properties/assignee.tsx +++ b/web/components/issues/issue-layouts/properties/assignee.tsx @@ -1,17 +1,11 @@ -import React from "react"; -// headless ui +import { FC, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; -// lucide icons import { ChevronDown, Search, X, Check } from "lucide-react"; -// mobx import { observer } from "mobx-react-lite"; // components import { Tooltip } from "@plane/ui"; // hooks import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; interface IFiltersOption { id: string; @@ -23,6 +17,7 @@ export interface IIssuePropertyAssignee { value?: any; onChange?: (id: any, data: any) => void; disabled?: boolean; + list?: any; className?: string; buttonClassName?: string; @@ -30,237 +25,228 @@ export interface IIssuePropertyAssignee { dropdownArrow?: boolean; } -export const IssuePropertyAssignee: React.FC = observer( - ({ - value, - onChange, - disabled, +export const IssuePropertyAssignee: FC = observer((props) => { + const { value, onChange, disabled, list, className, buttonClassName, optionsClassName, dropdownArrow = true } = props; - className, - buttonClassName, - optionsClassName, - dropdownArrow = true, - }) => { - const { project: projectStore }: RootStore = useMobxStore(); + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); - const [isOpen, setIsOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const options: IFiltersOption[] | [] = + (list && + list?.length > 0 && + list.map((_member: any) => ({ + id: _member?.member?.id, + title: _member?.member?.display_name, + avatar: _member?.member?.avatar && _member?.member?.avatar !== "" ? _member?.member?.avatar : null, + }))) || + []; - const options: IFiltersOption[] | [] = - (projectStore?.projectMembers && - projectStore?.projectMembers?.length > 0 && - projectStore?.projectMembers.map((_member: any) => ({ - id: _member?.member?.id, - title: _member?.member?.display_name, - avatar: _member?.member?.avatar && _member?.member?.avatar !== "" ? _member?.member?.avatar : null, - }))) || - []; + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + const selectedOption: IFiltersOption[] = + (value && value?.length > 0 && options.filter((_member: IFiltersOption) => value.includes(_member.id))) || []; - const selectedOption: IFiltersOption[] = - (value && value?.length > 0 && options.filter((_member: IFiltersOption) => value.includes(_member.id))) || []; + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_member: IFiltersOption) => + _member.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_member: IFiltersOption) => - _member.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; + const assigneeRenderLength = 5; - const assigneeRenderLength = 5; + return ( + _member.id) as string[]} + onChange={(data: string[]) => { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); - return ( - _member.id) as string[]} - onChange={(data: string[]) => { - if (onChange && selectedOption) onChange(data, selectedOption); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption && selectedOption?.length > 0 ? ( - <> - {selectedOption?.length > 1 ? ( - _label.title) || []).join(", ")} - > -
- {selectedOption.slice(0, assigneeRenderLength).map((_assignee) => ( -
- {_assignee && _assignee.avatar ? ( - {_assignee.title} - ) : ( - _assignee.title[0] - )} -
- ))} - {selectedOption.length > assigneeRenderLength && ( -
- +{selectedOption?.length - assigneeRenderLength} + return ( + <> + + {selectedOption && selectedOption?.length > 0 ? ( + <> + {selectedOption?.length > 1 ? ( + _label.title) || []).join(", ")} + > +
+ {selectedOption.slice(0, assigneeRenderLength).map((_assignee) => ( +
+ {_assignee && _assignee.avatar ? ( + {_assignee.title} + ) : ( + _assignee.title[0] + )} +
+ ))} + {selectedOption.length > assigneeRenderLength && ( +
+ +{selectedOption?.length - assigneeRenderLength} +
+ )} +
+
+ ) : ( + _label.title) || []).join(", ")} + > +
+
+ {selectedOption[0] && selectedOption[0].avatar ? ( + {selectedOption[0].title} + ) : ( +
+ {selectedOption[0].title[0]}
)}
- - ) : ( - _label.title) || []).join(", ")} - > -
-
- {selectedOption[0] && selectedOption[0].avatar ? ( - {selectedOption[0].title} - ) : ( -
- {selectedOption[0].title[0]} -
- )} -
-
{selectedOption[0].title}
-
-
- )} - - ) : ( -
Select option
- )} +
{selectedOption[0].title}
+
+
+ )} + + ) : ( + +
Select Assignees
+
+ )} - {dropdownArrow && !disabled && ( -
- -
- )} -
+ {dropdownArrow && !disabled && ( +
+ +
+ )} + -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} +
+ + {options && options.length > 0 ? ( + <> +
+
+
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || (value && value.length > 0 && value.includes(option?.id)) - ? "bg-custom-background-80" - : "" - } ${ - value && value.length > 0 && value.includes(option?.id) - ? "text-custom-text-100" - : "text-custom-text-200" - }` - } - > -
-
- {option && option.avatar ? ( - {option.title} - ) : ( -
- {option.title[0]} -
- )} -
-
{option.title}
- {value && value.length > 0 && value.includes(option?.id) && ( -
- +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active || (value && value.length > 0 && value.includes(option?.id)) + ? "bg-custom-background-80" + : "" + } ${ + value && value.length > 0 && value.includes(option?.id) + ? "text-custom-text-100" + : "text-custom-text-200" + }` + } + > +
+
+ {option && option.avatar ? ( + {option.title} + ) : ( +
+ {option.title[0]}
)}
- - )) - ) : ( - -

No matching results

-
- ) +
{option.title}
+ {value && value.length > 0 && value.includes(option?.id) && ( +
+ +
+ )} +
+
+ )) ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} - -
- - ); - }} - - ); - } -); + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} + +
+ + ); + }} + + ); +}); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx index 2056e67ce..48b6de507 100644 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -14,83 +14,86 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; // helpers import { renderDateFormat } from "helpers/date-time.helper"; -export interface IIssuePropertyStartDate { +export interface IIssuePropertyDate { value?: any; onChange?: (date: any) => void; disabled?: boolean; + placeHolder?: string; } -export const IssuePropertyStartDate: React.FC = observer(({ value, onChange, disabled }) => { - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); +export const IssuePropertyDate: React.FC = observer( + ({ value, onChange, disabled, placeHolder }) => { + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); - const [isOpen, setIsOpen] = React.useState(false); + const [isOpen, setIsOpen] = React.useState(false); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - return ( - - {({ open }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); + return ( + + {({ open }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); - return ( - <> - - -
-
- -
- {value ? ( - <> -
{value}
-
{ - if (onChange) onChange(null); - }} - > - -
- - ) : ( -
Select date
- )} -
-
-
- -
- + - {({ close }) => ( - { - if (onChange && val) { - onChange(renderDateFormat(val)); - close(); - } - }} - dateFormat="dd-MM-yyyy" - calendarClassName="h-full" - inline - /> - )} - -
- - ); - }} -
- ); -}); + +
+
+ +
+ {value ? ( + <> +
{value}
+
{ + if (onChange) onChange(null); + }} + > + +
+ + ) : ( +
{placeHolder ? placeHolder : `Select date`}
+ )} +
+
+ + +
+ + {({ close }) => ( + { + if (onChange && val) { + onChange(renderDateFormat(val)); + close(); + } + }} + dateFormat="dd-MM-yyyy" + calendarClassName="h-full" + inline + /> + )} + +
+ + ); + }} +
+ ); + } +); diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx index fc9ad5bc9..80b3d9615 100644 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -121,7 +121,9 @@ export const IssuePropertyEstimates: React.FC = observe
) : ( -
Select option
+ +
Select Estimates
+
)} {dropdownArrow && !disabled && ( diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index 189832615..bc9930c72 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,17 +1,11 @@ -import React from "react"; -// headless ui +import { FC, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; -// lucide icons import { ChevronDown, Search, X, Check } from "lucide-react"; -// mobx import { observer } from "mobx-react-lite"; // components import { Tooltip } from "@plane/ui"; // hooks import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; interface IFiltersOption { id: string; @@ -23,6 +17,7 @@ export interface IIssuePropertyLabels { value?: any; onChange?: (id: any, data: any) => void; disabled?: boolean; + list?: any; className?: string; buttonClassName?: string; @@ -30,205 +25,206 @@ export interface IIssuePropertyLabels { dropdownArrow?: boolean; } -export const IssuePropertyLabels: React.FC = observer( - ({ +export const IssuePropertyLabels: FC = observer((props) => { + const { value, onChange, disabled, + list, className, buttonClassName, optionsClassName, dropdownArrow = true, - }) => { - const { project: projectStore }: RootStore = useMobxStore(); + } = props; - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); - const [isOpen, setIsOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); - const options: IFiltersOption[] | [] = - (projectStore?.projectLabels && - projectStore?.projectLabels?.length > 0 && - projectStore?.projectLabels.map((_label: any) => ({ - id: _label?.id, - title: _label?.name, - color: _label?.color || null, - }))) || - []; + const options: IFiltersOption[] | [] = + (list && + list?.length > 0 && + list.map((_label: any) => ({ + id: _label?.id, + title: _label?.name, + color: _label?.color || null, + }))) || + []; - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - const selectedOption: IFiltersOption[] = - (value && value?.length > 0 && options.filter((_label: IFiltersOption) => value.includes(_label.id))) || []; + const selectedOption: IFiltersOption[] = + (value && value?.length > 0 && options.filter((_label: IFiltersOption) => value.includes(_label.id))) || []; - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_label: IFiltersOption) => - _label.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_label: IFiltersOption) => + _label.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; - return ( - _label.id) as string[]} - onChange={(data: string[]) => { - if (onChange && selectedOption) onChange(data, selectedOption); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); + return ( + _label.id) as string[]} + onChange={(data: string[]) => { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); - return ( - <> - + + {selectedOption && selectedOption?.length > 0 ? ( + <> + {selectedOption?.length === 1 ? ( + _label.title) || []).join(", ")} + > +
+
+
{selectedOption[0]?.title}
+
+ + ) : ( + _label.title) || []).join(", ")} + > +
+
+
{selectedOption?.length} Labels
+
+ + )} + + ) : ( + +
Select Labels
+
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} + + +
+ - {selectedOption && selectedOption?.length > 0 ? ( + {options && options.length > 0 ? ( <> - {selectedOption?.length === 1 ? ( - _label.title) || []).join(", ")} - > -
-
-
{selectedOption[0]?.title}
+
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > +
- - ) : ( - _label.title) || []).join(", ")} - > -
-
-
{selectedOption?.length} Labels
-
- - )} + )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active || (value && value.length > 0 && value.includes(option?.id)) + ? "bg-custom-background-80" + : "" + } ${ + value && value.length > 0 && value.includes(option?.id) + ? "text-custom-text-100" + : "text-custom-text-200" + }` + } + > +
+
+
{option.title}
+ {value && value.length > 0 && value.includes(option?.id) && ( +
+ +
+ )} +
+ + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
) : ( -
Select option
+

No options available.

)} - - {dropdownArrow && !disabled && ( -
- -
- )} - - -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || (value && value.length > 0 && value.includes(option?.id)) - ? "bg-custom-background-80" - : "" - } ${ - value && value.length > 0 && value.includes(option?.id) - ? "text-custom-text-100" - : "text-custom-text-200" - }` - } - > -
-
-
{option.title}
- {value && value.length > 0 && value.includes(option?.id) && ( -
- -
- )} -
- - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} - -
- - ); - }} - - ); - } -); +
+
+ + ); + }} + + ); +}); diff --git a/web/components/issues/issue-layouts/properties/priority.tsx b/web/components/issues/issue-layouts/properties/priority.tsx index 44ed22237..37404c11b 100644 --- a/web/components/issues/issue-layouts/properties/priority.tsx +++ b/web/components/issues/issue-layouts/properties/priority.tsx @@ -1,16 +1,11 @@ -import React from "react"; -// headless ui +import { FC, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; -// lucide icons import { ChevronDown, Search, X, Check, AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; -// mobx import { observer } from "mobx-react-lite"; // components import { Tooltip } from "@plane/ui"; // hooks import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// constants -import { ISSUE_PRIORITIES } from "constants/issue"; interface IFiltersOption { id: string; @@ -21,6 +16,7 @@ export interface IIssuePropertyPriority { value?: any; onChange?: (id: any, data: IFiltersOption) => void; disabled?: boolean; + list?: any; className?: string; buttonClassName?: string; @@ -54,171 +50,174 @@ const Icon = ({ priority }: any) => (
); -export const IssuePropertyPriority: React.FC = observer( - ({ +export const IssuePropertyPriority: FC = observer((props) => { + const { value, onChange, disabled, + list, className, buttonClassName, optionsClassName, dropdownArrow = true, - }) => { - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); + } = props; - const [isOpen, setIsOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); - const options: IFiltersOption[] | [] = - (ISSUE_PRIORITIES && - ISSUE_PRIORITIES?.length > 0 && - ISSUE_PRIORITIES.map((_priority: any) => ({ - id: _priority?.key, - title: _priority?.title, - }))) || - []; + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + const options: IFiltersOption[] | [] = + (list && + list?.length > 0 && + list.map((_priority: any) => ({ + id: _priority?.key, + title: _priority?.title, + }))) || + []; - const selectedOption: IFiltersOption | null | undefined = - (value && options.find((_priority: IFiltersOption) => _priority.id === value)) || null; + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_priority: IFiltersOption) => - _priority.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((_priority: IFiltersOption) => _priority.id === value)) || null; - return ( - { - if (onChange && selectedOption) onChange(data, selectedOption); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_priority: IFiltersOption) => + _priority.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; - return ( - <> - - {selectedOption ? ( - -
-
- -
-
{selectedOption?.title}
+ return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); + + return ( + <> + + {selectedOption ? ( + +
+
+
- - ) : ( -
Select option
- )} - - {dropdownArrow && !disabled && ( -
- +
{selectedOption?.title}
- )} - + + ) : ( + +
Select Priority
+
+ )} -
- - {options && options.length > 0 ? ( - <> -
-
- -
+ {dropdownArrow && !disabled && ( +
+ +
+ )} + -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} +
+ + {options && options.length > 0 ? ( + <> +
+
+
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `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.title}
- {selected && ( -
- -
- )} +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )} +
+ +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+
- )} - - )) - ) : ( - -

No matching results

-
- ) +
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} - -
- - ); - }} - - ); - } -); + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} + + ); +}); diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx index a3ee4e044..05cb375ad 100644 --- a/web/components/issues/issue-layouts/properties/state.tsx +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -1,17 +1,12 @@ -import React from "react"; -// headless ui +import { FC, useRef, useState } from "react"; import { Combobox } from "@headlessui/react"; -// lucide icons import { ChevronDown, Search, X, Check } from "lucide-react"; -// mobx import { observer } from "mobx-react-lite"; // components import { Tooltip, StateGroupIcon } from "@plane/ui"; // hooks import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; + // types import { IState } from "types"; @@ -26,6 +21,7 @@ export interface IIssuePropertyState { value?: any; onChange?: (id: any, data: IFiltersOption) => void; disabled?: boolean; + list?: any; className?: string; buttonClassName?: string; @@ -33,185 +29,186 @@ export interface IIssuePropertyState { dropdownArrow?: boolean; } -export const IssuePropertyState: React.FC = observer( - ({ +export const IssuePropertyState: FC = observer((props) => { + const { value, onChange, disabled, + list, className, buttonClassName, optionsClassName, dropdownArrow = true, - }) => { - const { project: projectStore }: RootStore = useMobxStore(); + } = props; - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); + const dropdownBtn = useRef(null); + const dropdownOptions = useRef(null); - const [isOpen, setIsOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const [isOpen, setIsOpen] = useState(false); + const [search, setSearch] = useState(""); - const options: IFiltersOption[] | [] = - (projectStore?.projectStates && - projectStore?.projectStates?.length > 0 && - projectStore?.projectStates.map((_state: IState) => ({ - id: _state?.id, - title: _state?.name, - group: _state?.group, - color: _state?.color || null, - }))) || - []; + const options: IFiltersOption[] | [] = + (list && + list?.length > 0 && + list.map((_state: IState) => ({ + id: _state?.id, + title: _state?.name, + group: _state?.group, + color: _state?.color || null, + }))) || + []; - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - const selectedOption: IFiltersOption | null | undefined = - (value && options.find((_state: IFiltersOption) => _state.id === value)) || null; + const selectedOption: IFiltersOption | null | undefined = + (value && options.find((_state: IFiltersOption) => _state.id === value)) || null; - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_state: IFiltersOption) => - _state.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; + const filteredOptions: IFiltersOption[] = + search === "" + ? options && options.length > 0 + ? options + : [] + : options && options.length > 0 + ? options.filter((_state: IFiltersOption) => + _state.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) + ) + : []; - return ( - { - if (onChange && selectedOption) onChange(data, selectedOption); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); + return ( + { + if (onChange && selectedOption) onChange(data, selectedOption); + }} + disabled={disabled} + > + {({ open }: { open: boolean }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); - return ( - <> - + + {selectedOption ? ( + +
+
+ +
+
{selectedOption?.title}
+
+
+ ) : ( + +
Select State
+
+ )} + + {dropdownArrow && !disabled && ( +
+ +
+ )} +
+ +
+ - {selectedOption ? ( - -
-
- 0 ? ( + <> +
+
+ +
+ +
+ setSearch(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} />
-
{selectedOption?.title}
+ + {search && search.length > 0 && ( +
setSearch("")} + className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" + > + +
+ )}
- - ) : ( -
Select option
- )} - {dropdownArrow && !disabled && ( -
- -
- )} - - -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `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.title}
- {selected && ( -
- -
- )} +
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `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 }) => ( +
+
+
- )} - - )) - ) : ( - -

No matching results

-
- ) +
{option.title}
+ {selected && ( +
+ +
+ )} +
+ )} +
+ )) ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} - -
- - ); - }} - - ); - } -); + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+ + ) : ( +

No options available.

+ )} +
+
+ + ); + }} + + ); +}); diff --git a/web/components/profile/index.ts b/web/components/profile/index.ts index 382994a38..247a10dc6 100644 --- a/web/components/profile/index.ts +++ b/web/components/profile/index.ts @@ -3,3 +3,5 @@ export * from "./navbar"; export * from "./profile-issues-view-options"; export * from "./profile-issues-view"; export * from "./sidebar"; + +export * from "./profile-issues-filter"; diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index e8988d670..cd43cc974 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import Link from "next/link"; // components -import { ProfileIssuesViewOptions } from "components/profile"; +import { ProfileIssuesFilter } from "components/profile"; // types import { UserAuth } from "types"; @@ -43,9 +43,7 @@ export const ProfileNavbar: React.FC = ({ memberRole }) => { const { workspaceSlug, userId } = router.query; const tabsList = - memberRole.isOwner || memberRole.isMember || memberRole.isViewer - ? [...viewerTabs, ...adminTabs] - : viewerTabs; + memberRole.isOwner || memberRole.isMember || memberRole.isViewer ? [...viewerTabs, ...adminTabs] : viewerTabs; return (
@@ -64,7 +62,7 @@ export const ProfileNavbar: React.FC = ({ memberRole }) => { ))}
- +
); }; diff --git a/web/components/profile/profile-issues-filter.tsx b/web/components/profile/profile-issues-filter.tsx new file mode 100644 index 000000000..ad02673c4 --- /dev/null +++ b/web/components/profile/profile-issues-filter.tsx @@ -0,0 +1,70 @@ +import { observer } from "mobx-react-lite"; +// components +import { DisplayFiltersSelection, FilterSelection, FiltersDropdown, LayoutSelection } from "components/issues"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; + +export const ProfileIssuesFilter = observer(() => { + const { workspace: workspaceStore, profileIssueFilters: profileIssueFiltersStore }: RootStore = useMobxStore(); + + const handleLayoutChange = (_layout: string) => + profileIssueFiltersStore.handleIssueFilters("userDisplayFilters", { layout: _layout }); + + const handleFilters = (key: any, value: any) => { + let updatesFilters: any = profileIssueFiltersStore?.userFilters; + updatesFilters = updatesFilters[key] || []; + if (updatesFilters && updatesFilters.length > 0 && updatesFilters.includes(value)) + updatesFilters = updatesFilters.filter((item: any) => item !== value); + else updatesFilters.push(value); + profileIssueFiltersStore.handleIssueFilters("userFilters", { [key]: updatesFilters }); + }; + + const handleDisplayFilters = (value: any) => profileIssueFiltersStore.handleIssueFilters("userDisplayFilters", value); + + const handleDisplayProperties = (value: any) => + profileIssueFiltersStore.handleIssueFilters("userDisplayProperties", value); + + const states = undefined; + const labels = workspaceStore.workspaceLabels || undefined; + const members = undefined; + + const activeLayout = profileIssueFiltersStore?.userDisplayFilters?.layout; + + return ( +
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} + /> + + + + + + + + +
+ ); +}); diff --git a/web/constants/issue.ts b/web/constants/issue.ts index 246fc20f5..188ac4b5b 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -220,6 +220,90 @@ export interface ILayoutDisplayFiltersOptions { export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { [pageType: string]: { [layoutType: string]: ILayoutDisplayFiltersOptions }; } = { + profile_issues: { + list: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + kanban: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, + archived_issues: { + list: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + kanban: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, + draft_issues: { + list: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + kanban: { + filters: ["priority", "state_group", "labels", "start_date", "target_date"], + display_properties: true, + display_filters: { + group_by: ["state_detail.group", "priority", "project", "labels", null], + order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"], + type: [null, "active", "backlog"], + }, + extra_options: { + access: true, + values: ["show_empty_groups"], + }, + }, + }, my_issues: { spreadsheet: { filters: ["priority", "state_group", "labels", "assignees", "created_by", "project", "start_date", "target_date"], diff --git a/web/helpers/issue.helper.ts b/web/helpers/issue.helper.ts index d0066c0e6..919699d69 100644 --- a/web/helpers/issue.helper.ts +++ b/web/helpers/issue.helper.ts @@ -83,7 +83,7 @@ export const handleIssuesMutation: THandleIssuesMutation = ( export const handleIssueQueryParamsByLayout = ( layout: TIssueLayouts | undefined, - viewType: "my_issues" | "issues" + viewType: "my_issues" | "issues" | "profile_issues" | "archived_issues" | "draft_issues" ): TIssueParams[] | null => { const queryParams: TIssueParams[] = []; diff --git a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx index a60116503..267679a89 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/assigned.tsx @@ -1,19 +1,66 @@ import React from "react"; - +import type { NextPage } from "next"; +import { useRouter } from "next/router"; +import useSWR from "swr"; +import { observer } from "mobx-react-lite"; // contexts import { ProfileIssuesContextProvider } from "contexts/profile-issues-context"; +// layouts import { ProfileAuthWrapper } from "layouts/profile-layout"; // components -import { ProfileIssuesView } from "components/profile"; -// types -import type { NextPage } from "next"; +import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/profile-issues-root"; +import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/profile-issues-root"; +// hooks +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; -const ProfileAssignedIssues: NextPage = () => ( - - - - - -); +// types + +const ProfileAssignedIssues: NextPage = observer(() => { + const { + workspace: workspaceStore, + project: projectStore, + profileIssueFilters: profileIssueFiltersStore, + profileIssues: profileIssuesStore, + }: RootStore = useMobxStore(); + + const router = useRouter(); + const { workspaceSlug, userId } = router.query as { + workspaceSlug: string; + userId: string; + }; + + useSWR(`PROFILE_ISSUES_${workspaceSlug}_${userId}`, async () => { + if (workspaceSlug && userId) { + // workspace labels + workspaceStore.setWorkspaceSlug(workspaceSlug); + await workspaceStore.fetchWorkspaceLabels(workspaceSlug); + await projectStore.fetchProjects(workspaceSlug); + + //profile issues + await profileIssuesStore.fetchIssues(workspaceSlug, userId, "assigned"); + } + }); + + const activeLayout = profileIssueFiltersStore.userDisplayFilters.layout; + + return ( + + + {profileIssuesStore.loader ? ( +
Loading...
+ ) : ( +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : null} +
+ )} +
+
+ ); +}); export default ProfileAssignedIssues; diff --git a/web/store/archived-issues/index.ts b/web/store/archived-issues/index.ts new file mode 100644 index 000000000..abfd52768 --- /dev/null +++ b/web/store/archived-issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; diff --git a/web/store/archived-issues/issue.store.ts b/web/store/archived-issues/issue.store.ts new file mode 100644 index 000000000..141805e23 --- /dev/null +++ b/web/store/archived-issues/issue.store.ts @@ -0,0 +1,187 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueService } from "services/issue"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IArchivedIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [project_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + // action + fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class ArchivedIssueStore implements IArchivedIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [project_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + // service + issueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + }); + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const projectId: string | null = this.rootStore?.project?.projectId; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + return this.issues?.[projectId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore?.issueFilter?.appliedFilters; + const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [projectId]: { + ...this.issues[projectId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/archived-issues/issue_filters.store.ts b/web/store/archived-issues/issue_filters.store.ts new file mode 100644 index 000000000..04a0da9bd --- /dev/null +++ b/web/store/archived-issues/issue_filters.store.ts @@ -0,0 +1,109 @@ +import { observable, computed, makeObservable } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; + +export interface IArchivedIssueFilterStore { + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class ArchivedIssueFilterStore implements IArchivedIssueFilterStore { + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + assignees: null, + created_by: null, + subscriber: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + state: this.userFilters?.state || undefined, + assignees: this.userFilters?.assignees || undefined, + created_by: this.userFilters?.created_by || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || "state", + order_by: this.userDisplayFilters?.order_by || "-created_at", + sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, + type: this.userDisplayFilters?.type || undefined, + sub_issue: this.userDisplayFilters?.sub_issue || true, + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + start_target_date: this.userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } +} diff --git a/web/store/draft-issues/index.ts b/web/store/draft-issues/index.ts new file mode 100644 index 000000000..abfd52768 --- /dev/null +++ b/web/store/draft-issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; diff --git a/web/store/draft-issues/issue.store.ts b/web/store/draft-issues/issue.store.ts new file mode 100644 index 000000000..c8afece07 --- /dev/null +++ b/web/store/draft-issues/issue.store.ts @@ -0,0 +1,187 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { IssueService } from "services/issue"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IDraftIssueStore { + loader: boolean; + error: any | null; + // issues + issues: { + [project_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + // action + fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class DraftIssueStore implements IDraftIssueStore { + loader: boolean = false; + error: any | null = null; + issues: { + [project_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + // service + issueService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + }); + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const projectId: string | null = this.rootStore?.project?.projectId; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + return this.issues?.[projectId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const projectId: string | null = issue?.project; + const issueType = this.getIssueType; + if (!projectId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; + }); + }; + + fetchIssues = async (workspaceSlug: string, projectId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore?.issueFilter?.appliedFilters; + const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params); + + const issueType = this.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [projectId]: { + ...this.issues[projectId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/draft-issues/issue_filters.store.ts b/web/store/draft-issues/issue_filters.store.ts new file mode 100644 index 000000000..2560c011b --- /dev/null +++ b/web/store/draft-issues/issue_filters.store.ts @@ -0,0 +1,109 @@ +import { observable, computed, makeObservable } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; + +export interface IDraftIssueFilterStore { + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + + // computed + appliedFilters: TIssueParams[] | null; +} + +export class DraftIssueFilterStore implements IDraftIssueFilterStore { + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + assignees: null, + created_by: null, + subscriber: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + state: this.userFilters?.state || undefined, + assignees: this.userFilters?.assignees || undefined, + created_by: this.userFilters?.created_by || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || "state", + order_by: this.userDisplayFilters?.order_by || "-created_at", + sub_group_by: this.userDisplayFilters?.sub_group_by || undefined, + type: this.userDisplayFilters?.type || undefined, + sub_issue: this.userDisplayFilters?.sub_issue || true, + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + start_target_date: this.userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } +} diff --git a/web/store/profile-issues/index.ts b/web/store/profile-issues/index.ts new file mode 100644 index 000000000..abfd52768 --- /dev/null +++ b/web/store/profile-issues/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./issue_filters.store"; diff --git a/web/store/profile-issues/issue.store.ts b/web/store/profile-issues/issue.store.ts new file mode 100644 index 000000000..75898c1b4 --- /dev/null +++ b/web/store/profile-issues/issue.store.ts @@ -0,0 +1,231 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// store +import { RootStore } from "../root"; +// types +import { IIssue } from "types"; +// services +import { UserService } from "services/user.service"; +import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; + +export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; +export type IIssueGroupedStructure = { [group_id: string]: IIssue[] }; +export type IIssueGroupWithSubGroupsStructure = { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; +}; +export type IIssueUnGroupedStructure = IIssue[]; + +export interface IProfileIssueStore { + loader: boolean; + error: any | null; + userId: string | null; + currentProfileTab: "assigned" | "created" | "subscribed" | null; + issues: { + [workspace_slug: string]: { + [user_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; + }; + // computed + getIssueType: IIssueType | null; + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; + // action + fetchIssues: (workspaceSlug: string, userId: string, type: "assigned" | "created" | "subscribed") => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; +} + +export class ProfileIssueStore implements IProfileIssueStore { + loader: boolean = true; + error: any | null = null; + userId: string | null = null; + currentProfileTab: "assigned" | "created" | "subscribed" | null = null; + issues: { + [workspace_slug: string]: { + [user_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + }; + } = {}; + // service + userService; + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observable + loader: observable.ref, + error: observable.ref, + currentProfileTab: observable.ref, + userId: observable.ref, + issues: observable.ref, + // computed + getIssueType: computed, + getIssues: computed, + // actions + fetchIssues: action, + updateIssueStructure: action, + }); + this.rootStore = _rootStore; + this.userService = new UserService(); + } + + get getIssueType() { + const groupedLayouts = ["kanban", "list", "calendar"]; + const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; + + const issueLayout = this.rootStore?.profileIssueFilters?.userDisplayFilters?.layout || null; + const issueGroup = this.rootStore?.profileIssueFilters?.userDisplayFilters?.group_by || null; + const issueSubGroup = this.rootStore?.profileIssueFilters?.userDisplayFilters?.sub_group_by || null; + if (!issueLayout) return null; + + const _issueState = groupedLayouts.includes(issueLayout) + ? issueGroup + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : "ungrouped" + : ungroupedLayouts.includes(issueLayout) + ? "ungrouped" + : null; + + return _issueState || null; + } + + get getIssues() { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + const issueType = this.getIssueType; + if (!workspaceSlug || !userId || !issueType) return null; + + return this.issues?.[workspaceSlug]?.[userId]?.[issueType] || null; + } + + updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { + const workspaceSlug: string | null = this.rootStore?.workspace?.workspaceSlug; + const userId: string | null = this.userId; + + const issueType = this.getIssueType; + if (!workspaceSlug || !userId || !issueType) return null; + + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.getIssues; + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }; + } + + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)); + } + + const orderBy = this.rootStore?.profileIssueFilters?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } + + runInAction(() => { + this.issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issues, + }, + }, + }; + }); + }; + + fetchIssues = async ( + workspaceSlug: string, + userId: string, + type: "assigned" | "created" | "subscribed" = "assigned" + ) => { + try { + this.loader = true; + this.error = null; + + this.currentProfileTab = type; + this.userId = userId; + + const issueType = this.getIssueType; + + let params: any = this.rootStore?.profileIssueFilters?.appliedFilters; + params = { + ...params, + assignees: undefined, + created_by: undefined, + subscriber: undefined, + }; + if (type === "assigned") params = params ? { ...params, assignees: userId } : { assignees: userId }; + else if (type === "created") params = params ? { ...params, created_by: userId } : { created_by: userId }; + else if (type === "subscribed") params = params ? { ...params, subscriber: userId } : { subscriber: userId }; + + const issueResponse = await this.userService.getUserProfileIssues(workspaceSlug, userId, params); + + if (issueType != null) { + const _issues = { + ...this.issues, + [workspaceSlug]: { + ...this.issues?.[workspaceSlug], + [userId]: { + ...this.issues?.[workspaceSlug]?.[userId], + [issueType]: issueResponse, + }, + }, + }; + + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error in issues", error); + this.loader = false; + this.error = error; + return error; + } + }; +} diff --git a/web/store/profile-issues/issue_filters.store.ts b/web/store/profile-issues/issue_filters.store.ts new file mode 100644 index 000000000..c1d352564 --- /dev/null +++ b/web/store/profile-issues/issue_filters.store.ts @@ -0,0 +1,137 @@ +import { observable, computed, makeObservable, action, autorun, runInAction } from "mobx"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; + +export interface IProfileIssueFilterStore { + userDisplayProperties: IIssueDisplayProperties; + userDisplayFilters: IIssueDisplayFilterOptions; + userFilters: IIssueFilterOptions; + // computed + appliedFilters: TIssueParams[] | null; + // action + handleIssueFilters: (type: "userFilters" | "userDisplayFilters" | "userDisplayProperties", params: any) => void; +} + +export class ProfileIssueFilterStore implements IProfileIssueFilterStore { + // observables + userFilters: IIssueFilterOptions = { + priority: null, + state_group: null, + labels: null, + start_date: null, + target_date: null, + }; + userDisplayFilters: IIssueDisplayFilterOptions = { + group_by: null, + order_by: "sort_order", + show_empty_groups: true, + type: null, + layout: "list", + }; + userDisplayProperties: any = { + assignee: true, + start_date: true, + due_date: true, + labels: true, + key: true, + priority: true, + state: true, + sub_issue_count: true, + link: true, + attachment_count: true, + estimate: true, + created_on: true, + updated_on: true, + }; + // root store + rootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + userFilters: observable.ref, + userDisplayFilters: observable.ref, + userDisplayProperties: observable.ref, + // computed + appliedFilters: computed, + // actions + handleIssueFilters: action, + }); + + this.rootStore = _rootStore; + + autorun(() => { + if (this.userFilters || this.userDisplayFilters || this.userDisplayProperties) { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const userId = this.rootStore.profileIssues?.userId; + if (workspaceSlug && userId && this.rootStore.profileIssues.currentProfileTab) { + console.log("autorun triggered"); + this.rootStore.profileIssues.fetchIssues( + workspaceSlug, + userId, + this.rootStore.profileIssues.currentProfileTab + ); + } + } + }); + } + + computedFilter = (filters: any, filteredParams: any) => { + const computedFilters: any = {}; + Object.keys(filters).map((key) => { + if (filters[key] != undefined && filteredParams.includes(key)) + computedFilters[key] = + typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(","); + }); + + return computedFilters; + }; + + get appliedFilters(): TIssueParams[] | null { + if (!this.userFilters || !this.userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userFilters?.priority || undefined, + state_group: this.userFilters?.state_group || undefined, + labels: this.userFilters?.labels || undefined, + start_date: this.userFilters?.start_date || undefined, + target_date: this.userFilters?.target_date || undefined, + group_by: this.userDisplayFilters?.group_by || undefined, + order_by: this.userDisplayFilters?.order_by || "-created_at", + show_empty_groups: this.userDisplayFilters?.show_empty_groups || true, + type: this.userDisplayFilters?.type || undefined, + assignees: undefined, + created_by: undefined, + subscriber: undefined, + }; + + const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "profile_issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } + + handleIssueFilters = (type: "userFilters" | "userDisplayFilters" | "userDisplayProperties", params: any) => { + if (type === "userFilters") { + const updatedFilters = { ...this.userFilters, ...params }; + runInAction(() => { + this.userFilters = updatedFilters; + }); + } + if (type === "userDisplayFilters") { + const updatedFilters = { ...this.userDisplayFilters, ...params }; + runInAction(() => { + this.userDisplayFilters = updatedFilters; + }); + } + if (type === "userDisplayProperties") { + const updatedFilters = { ...this.userDisplayProperties, ...params }; + runInAction(() => { + this.userDisplayProperties = updatedFilters; + }); + } + }; +} diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index 48535993a..7761ec5aa 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -31,6 +31,7 @@ export interface IProjectStore { // computed searchedProjects: IProject[]; + workspaceProjects: IProject[]; projectStatesByGroups: IStateResponse | null; projectStates: IState[] | null; projectLabels: IIssueLabels[] | null; @@ -119,6 +120,7 @@ export class ProjectStore implements IProjectStore { // computed searchedProjects: computed, + workspaceProjects: computed, projectStatesByGroups: computed, projectStates: computed, projectLabels: computed, @@ -176,6 +178,11 @@ export class ProjectStore implements IProjectStore { ); } + get workspaceProjects() { + if (!this.rootStore.workspace.workspaceSlug) return []; + return this.projects?.[this.rootStore.workspace.workspaceSlug]; + } + get joinedProjects() { if (!this.rootStore.workspace.workspaceSlug) return []; return this.projects?.[this.rootStore.workspace.workspaceSlug]?.filter((p) => p.is_member); diff --git a/web/store/root.ts b/web/store/root.ts index 827c1a1ee..803fa3894 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -1,4 +1,3 @@ -// mobx lite import { enableStaticRendering } from "mobx-react-lite"; // store imports import UserStore from "store/user.store"; @@ -53,6 +52,19 @@ import { IGlobalViewIssuesStore, IGlobalViewsStore, } from "store/global-view"; +import { + ProfileIssueStore, + IProfileIssueStore, + ProfileIssueFilterStore, + IProfileIssueFilterStore, +} from "store/profile-issues"; +import { + ArchivedIssueStore, + IArchivedIssueStore, + ArchivedIssueFilterStore, + IArchivedIssueFilterStore, +} from "store/archived-issues"; +import { DraftIssueStore, IDraftIssueStore, DraftIssueFilterStore, IDraftIssueFilterStore } from "store/draft-issues"; import { IInboxFiltersStore, IInboxIssueDetailsStore, @@ -102,6 +114,15 @@ export class RootStore { globalViewIssues: IGlobalViewIssuesStore; globalViewFilters: IGlobalViewFiltersStore; + profileIssues: IProfileIssueStore; + profileIssueFilters: IProfileIssueFilterStore; + + archivedIssues: IArchivedIssueStore; + archivedIssueFilters: IArchivedIssueFilterStore; + + draftIssues: IDraftIssueStore; + draftIssueFilters: IDraftIssueFilterStore; + inbox: IInboxStore; inboxIssues: IInboxIssuesStore; inboxIssueDetails: IInboxIssueDetailsStore; @@ -143,6 +164,15 @@ export class RootStore { this.globalViewIssues = new GlobalViewIssuesStore(this); this.globalViewFilters = new GlobalViewFiltersStore(this); + this.profileIssues = new ProfileIssueStore(this); + this.profileIssueFilters = new ProfileIssueFilterStore(this); + + this.archivedIssues = new ArchivedIssueStore(this); + this.archivedIssueFilters = new ArchivedIssueFilterStore(this); + + this.draftIssues = new DraftIssueStore(this); + this.draftIssueFilters = new DraftIssueFilterStore(this); + this.inbox = new InboxStore(this); this.inboxIssues = new InboxIssuesStore(this); this.inboxIssueDetails = new InboxIssueDetailsStore(this); diff --git a/yarn.lock b/yarn.lock index 8b32e5390..6a05e0fde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2835,7 +2835,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@18.2.0": +"@types/react@*", "@types/react@18.0.15", "@types/react@18.0.28", "@types/react@18.2.0", "@types/react@^18.2.5": version "18.2.0" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.0.tgz#15cda145354accfc09a18d2f2305f9fc099ada21" integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== @@ -2844,33 +2844,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@18.0.15": - version "18.0.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe" - integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@18.0.28": - version "18.0.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" - integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^18.2.5": - version "18.2.28" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.28.tgz#86877465c0fcf751659a36c769ecedfcfacee332" - integrity sha512-ad4aa/RaaJS3hyGz0BGegdnSRXQBkd1CCYDCdNjBPg90UUpLgo+WlJqb9fMYUxtehmzF3PJaTWqRZjko6BRzBg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/reactcss@*": version "1.2.6" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.6.tgz#133c1e7e896f2726370d1d5a26bf06a30a038bcc"